6 Commits
1.0.3 ... 1.1.0

Author SHA1 Message Date
9afa00f61f add minimal sbom converter
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-07-09 11:26:50 +02:00
bd92d8eb87 drop plantuml snippet
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-09 08:43:37 +02:00
5a1d6903e8 test plantuml integration
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-25 16:05:48 +02:00
67bab6710c documentation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-20 10:38:12 +02:00
f55c3da3ef solve conflicting option
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-06-11 10:40:53 +02:00
f50d821aec verbose switch
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-06-11 09:59:13 +02:00
5 changed files with 217 additions and 10 deletions

View File

@ -32,6 +32,7 @@ WORKDIR $APP_DIR
COPY src/requirements.txt . COPY src/requirements.txt .
COPY src/sbom-dt-dd.py . COPY src/sbom-dt-dd.py .
COPY src/converter.py .
COPY src/entrypoint.sh . COPY src/entrypoint.sh .
COPY dependencytrack-client/ ./dependencytrack-client COPY dependencytrack-client/ ./dependencytrack-client
COPY defectdojo-client/ ./defectdojo-client COPY defectdojo-client/ ./defectdojo-client

100
readme.md
View File

@ -1,6 +1,96 @@
# Python Client Packages for the DependencyTrack and DefectDojo API # DependencyTrack and DefectDojo Automation
## Download the OpenAPI definitions
## Using
### Distribution
The glue logic comes in a docker image and can be started as a docker container. Due to the dependencies, especially the ones related to the
APIs of DependencyTrack and DefectDojo this approach has been chosen.
The image is available at
```
quay.io/wollud1969/dtrack-defectdojo-automation
```
and at
```
devnexus.krohne.com:18079/repository/docker-krohne/dtrack-defectdojo-automation
```
The tag to be used at the moment is `1.0.5`.
### Start script
On Linux I've created two files to start the beast:
env-sbom-dd-dt
```
DTRACK_API_URL=https://dtrack-api-rd.krohne.com
DEFECTDOJO_URL=https://defectdojo-rd.krohne.com
DTRACK_TOKEN=...
DEFECTDOJO_TOKEN=...
```
The correct values for the tokens must be set here, obviously.
sbom-dd-dt.sh
```
#!/bin/bash
docker run -t -v $PWD:/work --rm --env-file ~/env-sbom-dt-dd devnexus.krohne.com:18079/repository/docker-krohne/dtrack-defectdojo-automation:1.0.5 "$@"
```
I've both files directly in my home-dir.
### File locations
When using the container and the script, you must consider that the container has no full access to your filesystem and you need to mount required parts of your filesystem into the container. In the above script I do this with the option `-v $PWD:/work`. This option mounts the current directory (the one from where you are starting the script and thus the container) into the directory `/work` within the container.
This is required when scanning a directory or uploading a prepared SBOM file.
### Options of the container/script
The container has the glue logic script as entrypoint. To find out about the options, call
```
dehottgw@DE01RDDEV01:~$ docker run -t -v $PWD:/work --rm --env-file ~/env-sbom-dt-dd devnexus.krohne.com:18079/repository/docker-krohne/dtrack-defectdojo-automation:1.0.5 -- -h
usage: sbom-dt-dd.py [-h] --name NAME --version VERSION --description DESCRIPTION --type TYPE --classifier
{APPLICATION,FRAMEWORK,LIBRARY,CONTAINER,OPERATING_SYSTEM,DEVICE,FIRMWARE,FILE,PLATFORM,DEVICE_DRIVER,MACHINE_LEARNING_MODEL,DATA}
[--uploadsbom] [--sbomfile SBOMFILE] [--target TARGET] [--verbose]
sbom-dt-dd.py: error: the following arguments are required: --name/-n, --version/-v, --description/-d, --type/-t, --classifier/-c
dehottgw@DE01RDDEV01:~$
```
Note the double-dash at the end of the commandline before the `-h`. It is necessary, otherwise the `-h` would be considered as an option for the docker command itself.
### SBOM upload example
For this example I've a file `combined-sbom.json` in the directory `software1`:
```
cd software1/
~/sbom-dt-dd.sh --name software1-server --version 0.0.1 --description "Server software for the Software1 platform" --type 1 --classifier APPLICATION --uploadsbom --sbomfile /work/combined-sbom.json -V
```
## Building
### Python Client Packages for the DependencyTrack and DefectDojo API
#### Download the OpenAPI definitions
``` ```
curl https://dtrack-api.hottis.de/api/openapi.json \ curl https://dtrack-api.hottis.de/api/openapi.json \
@ -10,7 +100,7 @@ curl https://defectdojo.hottis.de/api/v2/oa3/schema/?format=json \
``` ```
## Naive Generation of the Client Package for DefectDojo #### Naive Generation of the Client Package for DefectDojo
``` ```
docker run \ docker run \
@ -28,7 +118,7 @@ docker run \
For DefectDojo the naive code generation works. For DefectDojo the naive code generation works.
## Naive Generation of the Client Package for DependencyTrack #### Naive Generation of the Client Package for DependencyTrack
``` ```
docker run \ docker run \
@ -43,7 +133,7 @@ docker run \
--package-name dependencytrack_api --package-name dependencytrack_api
``` ```
## Fixed Generation of the Client Package for DependencyTrack #### Fixed Generation of the Client Package for DependencyTrack
In the OpenAPI definition of DependencyTrack a regex is used which is not understood by Python's In the OpenAPI definition of DependencyTrack a regex is used which is not understood by Python's
default regex implement `re`, which in turn is hardwired in the openapi-generator provided code. default regex implement `re`, which in turn is hardwired in the openapi-generator provided code.

96
src/converter.py Normal file
View File

@ -0,0 +1,96 @@
from loguru import logger
import yaml
import uuid
from packageurl import PackageURL
from cyclonedx.builder.this import this_component as cdx_lib_component
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model.contact import OrganizationalEntity
from cyclonedx.model import XsUri
from cyclonedx.output.json import JsonV1Dot5
class MyLocalConverterException(Exception): pass
def __converterClassifierToComponentType(classifier):
componentType = ''
match classifier:
case 'APPLICATION':
componentType = ComponentType.APPLICATION
case 'FRAMEWORK':
componentType = ComponentType.FRAMEWORK
case 'LIBRARY':
componentType = ComponentType.LIBRARY
case 'CONTAINER':
componentType = ComponentType.CONTAINER
case 'OPERATING_SYSTEM':
componentType = ComponentType.OPERATING_SYSTEM
case 'DEVICE':
componentType = ComponentType.DEVICE
case 'FIRMWARE':
componentType = ComponentType.FIRMWARE
case 'FILE':
componentType = ComponentType.FILE
case 'PLATFORM':
componentType = ComponentType.PLATFORM
case 'DEVICE_DRIVER':
componentType = ComponentType.DEVICE_DRIVER
case 'MACHINE_LEARNING_MODEL':
componentType = ComponentType.MACHINE_LEARNING_MODEL
case 'DATA':
componentType = ComponentType.DATA
case _:
raise MyLocalConverterException(f"No componentType for {classifier} found")
return componentType
def minimalSbomFormatConverter(minimalSbom, classifier):
logger.info(f"Minimal input: {minimalSbom}")
lc_factory = LicenseFactory()
minimalSbomObject = yaml.safe_load(minimalSbom)
logger.debug(f"{minimalSbomObject=}")
bom = Bom()
bom.metadata.tools.components.add(cdx_lib_component())
bom.metadata.tools.components.add(Component(
name='sbom-dt-dd',
type=ComponentType.APPLICATION
))
bom.metadata.component = root_component = Component(
name=minimalSbomObject['product'],
type=__converterClassifierToComponentType(classifier),
version=minimalSbomObject['version'],
licenses=[lc_factory.make_from_string(minimalSbomObject['license'])],
supplier=OrganizationalEntity(
name=minimalSbomObject['supplier']['name'],
urls=[XsUri(minimalSbomObject['supplier']['url'])]
),
bom_ref = f"urn:uuid:{uuid.uuid4()}"
)
for minimalComponentDescription in minimalSbomObject['components']:
component = Component(
type=ComponentType.LIBRARY,
name=minimalComponentDescription['name'],
version=minimalComponentDescription['version'],
licenses=[lc_factory.make_from_string(minimalComponentDescription['license'])],
bom_ref = f"urn:uuid:{uuid.uuid4()}"
)
if 'cpe' in minimalComponentDescription:
component.cpe = minimalComponentDescription['cpe']
if 'purl' in minimalComponentDescription:
component.purl = PackageURL.from_string(minimalComponentDescription['purl'])
bom.components.add(component)
bom.register_dependency(root_component, [component])
outputSbom = JsonV1Dot5(bom).output_as_string(indent=2)
logger.info(outputSbom)
raise Exception("Conversion aborted")

View File

@ -1,3 +1,5 @@
regex==2024.11.6 regex==2024.11.6
loguru==0.7.3 loguru==0.7.3
PyYAML==6.0.2
cyclonedx-python-lib==10.4.1

View File

@ -12,11 +12,16 @@ from dateutil.relativedelta import relativedelta
import dependencytrack_api import dependencytrack_api
from dependencytrack_api.rest import ApiException as DependencyTrackApiException from dependencytrack_api.rest import ApiException as DependencyTrackApiException
from converter import minimalSbomFormatConverter
class MyLocalException(Exception): pass class MyLocalException(Exception): pass
def executeApiCall(apiClient, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams=[]): def executeApiCall(apiClient, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams=[]):
try: try:
logger.info(f"Calling {ApiClass}.{EndpointMethod} with {RequestClass} ({additionalParams}, {requestParams})") logger.info(f"Calling {ApiClass=}.{EndpointMethod=} with {RequestClass=})")
if VERBOSE:
logger.debug(f"{additionalParams=}, {requestParams=}")
instance = ApiClass(apiClient) instance = ApiClass(apiClient)
if RequestClass: if RequestClass:
request = RequestClass(**requestParams) request = RequestClass(**requestParams)
@ -48,13 +53,9 @@ def generateSBOM(target='.', name='dummyName', version='0.0.0'):
try: try:
DTRACK_API_URL = os.environ["DTRACK_API_URL"] DTRACK_API_URL = os.environ["DTRACK_API_URL"]
logger.debug(f"{DTRACK_API_URL=}")
DTRACK_TOKEN = os.environ["DTRACK_TOKEN"] DTRACK_TOKEN = os.environ["DTRACK_TOKEN"]
logger.debug(f"{DTRACK_TOKEN=}")
DEFECTDOJO_URL = os.environ["DEFECTDOJO_URL"] DEFECTDOJO_URL = os.environ["DEFECTDOJO_URL"]
logger.debug(f"{DEFECTDOJO_URL=}")
DEFECTDOJO_TOKEN = os.environ["DEFECTDOJO_TOKEN"] DEFECTDOJO_TOKEN = os.environ["DEFECTDOJO_TOKEN"]
logger.debug(f"{DEFECTDOJO_TOKEN=}")
except KeyError as e: except KeyError as e:
raise Exception(f"Env variable {e} is shall be set") raise Exception(f"Env variable {e} is shall be set")
@ -86,9 +87,18 @@ parser.add_argument('--uploadsbom', '-U',
parser.add_argument('--sbomfile', '-F', parser.add_argument('--sbomfile', '-F',
help='Filename of existing SBOM file to upload, use together with -U, do not use together with -T', help='Filename of existing SBOM file to upload, use together with -U, do not use together with -T',
required=False) required=False)
parser.add_argument('--minimalsbomformat', '-K',
help='SBOM file comes in dedicated minimal format and will be converted into cyclonedx before uploading',
action='store_true',
default=False)
parser.add_argument('--target', '-T', parser.add_argument('--target', '-T',
help='Target to scan, either path name for sources or docker image tag', help='Target to scan, either path name for sources or docker image tag',
required=False) required=False)
parser.add_argument('--verbose', '-V',
help='A lot of debug output',
required=False,
action='store_true',
default=False)
args = parser.parse_args() args = parser.parse_args()
projectName = args.name projectName = args.name
projectVersion = args.version projectVersion = args.version
@ -99,9 +109,12 @@ projectClassifier = args.classifier
uploadSbomFlag = args.uploadsbom uploadSbomFlag = args.uploadsbom
if uploadSbomFlag: if uploadSbomFlag:
sbomFileName = args.sbomfile sbomFileName = args.sbomfile
minimalSbomFormat = args.minimalsbomformat
else: else:
target = args.target target = args.target
VERBOSE = args.verbose
# ---- main starts here -------------------------------------------------------------------------------------------------- # ---- main starts here --------------------------------------------------------------------------------------------------
@ -110,6 +123,11 @@ if uploadSbomFlag:
logger.info(f"Reading SBOM from file {sbomFileName}") logger.info(f"Reading SBOM from file {sbomFileName}")
with open(sbomFileName, 'r') as sbomFile: with open(sbomFileName, 'r') as sbomFile:
sbom = sbomFile.read() sbom = sbomFile.read()
logger.info("SBOM file read.")
if minimalSbomFormat:
logger.info("Start converting from minimal format into cyclonedx")
sbom = minimalSbomFormatConverter(sbom, projectClassifier)
logger.info("Converted")
logger.info("Done.") logger.info("Done.")
else: else:
# ------- generate SBOM ------------ # ------- generate SBOM ------------