Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions docs/source/acknowledgement-annotations-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Acknowledgement Annotations in SBOM Output

This document shows how the `acknowledgement` field from `scanoss.json` BOM operations
is exported to CycloneDX and SPDX using their native **annotations** support.

## scanoss.json (input)

The `acknowledgement` and `date` fields on BOM entries capture the decision and when it was made:

```json
{
"bom": {
"include": [
{
"path": "src/lib/component.js",
"purl": "pkg:npm/lodash@4.17.21",
"comment": "Vendored copy confirmed",
"acknowledgement": "Confirmed: lodash 4.17.21 vendored under src/lib",
"date": "2026-03-15T10:30:00Z"
}
],
"replace": [
{
"path": "src/utils/helper.js",
"purl": "pkg:npm/old-lib@1.0.0",
"replace_with": "pkg:npm/new-lib@2.0.0",
"license": "MIT",
"comment": "Upgrade to newer version",
"acknowledgement": "Verified upstream project is the correct attribution",
"date": "2026-03-10T14:00:00Z"
}
]
}
}
```

## CycloneDX 1.6 export

Annotations are a **top-level array** in the BOM. Each annotation references components
via `subjects` (using `bom-ref`) and records the annotator as a service.

Reference: [CycloneDX 1.6 Annotations](https://cyclonedx.org/docs/1.6/json/#annotations)

```json
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"timestamp": "2026-03-23T12:00:00Z",
"tools": [
{
"vendor": "SCANOSS",
"name": "scanoss-py",
"version": "1.49.0"
}
]
},
"services": [
{
"bom-ref": "scanoss-scanner",
"name": "SCANOSS Scanner",
"provider": { "name": "SCANOSS" }
}
],
"components": [
{
"type": "library",
"bom-ref": "pkg:npm/lodash@4.17.21",
"name": "lodash",
"version": "4.17.21",
"purl": "pkg:npm/lodash@4.17.21",
"licenses": [{ "id": "MIT" }]
},
{
"type": "library",
"bom-ref": "pkg:npm/new-lib@2.0.0",
"name": "new-lib",
"version": "2.0.0",
"purl": "pkg:npm/new-lib@2.0.0",
"licenses": [{ "id": "MIT" }]
}
],
"annotations": [
{
"subjects": ["pkg:npm/lodash@4.17.21"],
"annotator": { "service": { "bom-ref": "scanoss-scanner" } },
"timestamp": "2026-03-15T10:30:00Z",
"text": "Confirmed: lodash 4.17.21 vendored under src/lib"
},
{
"subjects": ["pkg:npm/new-lib@2.0.0"],
"annotator": { "service": { "bom-ref": "scanoss-scanner" } },
"timestamp": "2026-03-10T14:00:00Z",
"text": "Verified upstream project is the correct attribution"
}
]
}
```

## SPDX 2.3 export

Annotations are also **separate entries** that reference packages via their `SPDXID`.
The annotator is identified as a tool.

Reference: [SPDX 2.3 Annotations](https://spdx.github.io/spdx-spec/v2.3/annotations/)

```json
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "SCANOSS-SBOM",
"creationInfo": {
"created": "2026-03-23T12:00:00Z",
"creators": ["Tool: scanoss-py-1.49.0"]
},
"documentNamespace": "https://spdx.org/spdxdocs/scanoss-py-1.49.0-abc123",
"documentDescribes": ["SPDXRef-a1b2c3", "SPDXRef-d4e5f6"],
"packages": [
{
"name": "lodash",
"SPDXID": "SPDXRef-a1b2c3",
"versionInfo": "4.17.21",
"downloadLocation": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"licenseConcluded": "MIT",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/lodash@4.17.21"
}
]
},
{
"name": "new-lib",
"SPDXID": "SPDXRef-d4e5f6",
"versionInfo": "2.0.0",
"downloadLocation": "https://registry.npmjs.org/new-lib/-/new-lib-2.0.0.tgz",
"licenseConcluded": "MIT",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/new-lib@2.0.0"
}
]
}
],
"annotations": [
{
"annotator": "Tool: scanoss-py-1.49.0",
"annotationDate": "2026-03-15T10:30:00Z",
"annotationType": "OTHER",
"SPDXID": "SPDXRef-a1b2c3",
"comment": "Confirmed: lodash 4.17.21 vendored under src/lib"
},
{
"annotator": "Tool: scanoss-py-1.49.0",
"annotationDate": "2026-03-10T14:00:00Z",
"annotationType": "OTHER",
"SPDXID": "SPDXRef-d4e5f6",
"comment": "Verified upstream project is the correct attribution"
}
]
}
```
38 changes: 34 additions & 4 deletions src/scanoss/cyclonedx.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from cyclonedx.validation.json import JsonValidator

from . import __version__
from .scanoss_settings import find_best_match
from .scanossbase import ScanossBase
from .spdxlite import SpdxLite

Expand All @@ -42,13 +43,14 @@ class CycloneDx(ScanossBase):
Handle all interaction with CycloneDX formatting
"""

def __init__(self, debug: bool = False, output_file: str = None):
def __init__(self, debug: bool = False, output_file: str = None, scanoss_settings=None):
"""
Initialise the CycloneDX class
"""
super().__init__(debug)
self.output_file = output_file
self.debug = debug
self.scanoss_settings = scanoss_settings
self._spdx = SpdxLite(debug=debug)

def parse(self, data: dict): # noqa: PLR0912, PLR0915
Expand Down Expand Up @@ -100,6 +102,7 @@ def parse(self, data: dict): # noqa: PLR0912, PLR0915
fdl.append({'id': name})
dc.append(name)
fd['licenses'] = fdl
fd['_file_path'] = f
cdx[purl] = fd
else:
purls = d.get('purl')
Expand Down Expand Up @@ -158,6 +161,7 @@ def parse(self, data: dict): # noqa: PLR0912, PLR0915
continue
fdl.append({'id': name})
fd['licenses'] = fdl
fd['_file_path'] = f
cdx[purl] = fd
# self.print_stderr(f'VD: {vdx}')
# self.print_stderr(f'CDX: {cdx}')
Expand Down Expand Up @@ -200,13 +204,12 @@ def produce_from_json(self, data: dict, output_file: str = None) -> tuple[bool,
self.print_msg('Warning: Empty scan results - generating minimal CycloneDX SBOM with no components.')
self._spdx.load_license_data() # Load SPDX license name data for later reference
#
# Using CDX version 1.4: https://cyclonedx.org/docs/1.4/json/
# Using CDX version 1.5: https://cyclonedx.org/docs/1.5/json/
# Validate using: https://github.com/CycloneDX/cyclonedx-cli
# cyclonedx-cli validate --input-format json --input-version v1_4 --fail-on-errors --input-file cdx.json
#
data = {
'bomFormat': 'CycloneDX',
'specVersion': '1.4',
'specVersion': '1.5',
'serialNumber': f'urn:uuid:{uuid.uuid4()}',
'version': 1,
'metadata': {
Expand Down Expand Up @@ -255,6 +258,33 @@ def produce_from_json(self, data: dict, output_file: str = None) -> tuple[bool,
c_data['cpe'] = cpe
data['components'].append(c_data)
# End for loop
# Build annotations from BOM rules via ScanossSettings
annotations = []
if self.scanoss_settings:
all_entries = (self.scanoss_settings.get_bom_include()
+ self.scanoss_settings.get_bom_replace())
entries_with_ack = [e for e in all_entries if e.acknowledgement]
if entries_with_ack:
org = self.scanoss_settings.get_organization()
for purl in cdx:
comp = cdx.get(purl)
file_path = comp.get('_file_path', '')
match = find_best_match(file_path, [purl], entries_with_ack)
if match:
ts = match.timestamp
if not ts:
self.print_stderr(
f'Warning: No timestamp for annotation on {purl}, using current time'
)
ts = data['metadata']['timestamp']
annotations.append({
'subjects': [purl],
'text': match.acknowledgement,
'timestamp': ts,
'annotator': {'organization': {'name': org}},
})
if annotations:
data['annotations'] = annotations
if vdx:
for vuln_id in vdx:
vulns = vdx.get(vuln_id)
Expand Down
Loading
Loading