Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
9afa00f61f
|
|||
bd92d8eb87
|
|||
5a1d6903e8
|
|||
67bab6710c
|
|||
f55c3da3ef
|
|||
f50d821aec
|
|||
609f33b181
|
|||
7c8e1156aa
|
|||
226456ccd2
|
|||
227ef294d3
|
|||
a14e0ab2c5
|
|||
471fcb2177
|
|||
0d4ac4022a
|
|||
405d66cdcb
|
|||
a32d9fd643
|
|||
7f394f82ee
|
|||
c8577edf0c
|
|||
02aba34391
|
|||
1fb4c387a7
|
|||
92b61fdae0
|
@ -82,11 +82,11 @@ dockerize:
|
|||||||
script:
|
script:
|
||||||
- tar -xzf defectdojo-api-client.tgz
|
- tar -xzf defectdojo-api-client.tgz
|
||||||
- tar -xzf dtrack-api-client.tgz
|
- tar -xzf dtrack-api-client.tgz
|
||||||
- docker build --tag $IMAGE_NAME:latest
|
- docker build --build-arg ADDITIONAL_CA_URL="$KROHNE_CA_URL"
|
||||||
|
--build-arg ADDITIONAL_CA_CHECKSUM=$KROHNE_CA_CHECKSUM
|
||||||
|
--tag $IMAGE_NAME:latest
|
||||||
--tag $IMAGE_NAME:$CI_COMMIT_SHA
|
--tag $IMAGE_NAME:$CI_COMMIT_SHA
|
||||||
--tag $IMAGE_NAME:$CI_COMMIT_TAG
|
--tag $IMAGE_NAME:$CI_COMMIT_TAG
|
||||||
--build-arg "CUSTOM_CA_URL=$KROHNE_CA_URL"
|
|
||||||
--build-arg "CUSTOM_CA_CHECKSUM=$KROHNE_CA_CHECKSUM"
|
|
||||||
.
|
.
|
||||||
- docker login -u $NEXUS_USER -p $NEXUS_PASSWORD $REGISTRY
|
- docker login -u $NEXUS_USER -p $NEXUS_PASSWORD $REGISTRY
|
||||||
- docker push $IMAGE_NAME:latest
|
- docker push $IMAGE_NAME:latest
|
||||||
|
14
Dockerfile
14
Dockerfile
@ -6,18 +6,21 @@ ENV DEFECTDOJO_URL=""
|
|||||||
ENV DEFECTDOJO_TOKEN=""
|
ENV DEFECTDOJO_TOKEN=""
|
||||||
|
|
||||||
ARG APP_DIR=/opt/app
|
ARG APP_DIR=/opt/app
|
||||||
ARG ADDITIONAL_CA_URL=""
|
ARG ADDITIONAL_CA_URL="x"
|
||||||
ARG ADDITIONAL_CA_CHECKSUM=""
|
ARG ADDITIONAL_CA_CHECKSUM="y"
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
|
set -e &&\
|
||||||
apk add --no-cache syft &&\
|
apk add --no-cache syft &&\
|
||||||
adduser -s /bin/sh -D user &&\
|
adduser -s /bin/sh -D user &&\
|
||||||
mkdir -p $APP_DIR &&\
|
mkdir -p $APP_DIR &&\
|
||||||
chown user:user $APP_DIR &&\
|
chown user:user $APP_DIR &&\
|
||||||
if [ "$ADDITIONAL_CA_URL" != "" -a "$ADDITIONAL_CA_CHECKSUM" != "" ]; then \
|
echo $ADDITIONAL_CA_URL &&\
|
||||||
cd /usr/share/ca-certificates; \
|
echo $ADDITIONAL_CA_CHECKSUM &&\
|
||||||
|
if [ "$ADDITIONAL_CA_URL" != "x" ]; then \
|
||||||
|
cd /usr/local/share/ca-certificates; \
|
||||||
wget --no-check-certificate -O custom-ca.crt $ADDITIONAL_CA_URL; \
|
wget --no-check-certificate -O custom-ca.crt $ADDITIONAL_CA_URL; \
|
||||||
echo "a921e440a742f1e67c7714306e2c0d76 custom-ca.crt" | md5sum -c; \
|
echo "$ADDITIONAL_CA_CHECKSUM custom-ca.crt" | md5sum -c; \
|
||||||
/usr/sbin/update-ca-certificates; \
|
/usr/sbin/update-ca-certificates; \
|
||||||
echo "custom ca added"; \
|
echo "custom ca added"; \
|
||||||
else \
|
else \
|
||||||
@ -29,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
100
readme.md
@ -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
96
src/converter.py
Normal 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")
|
||||||
|
|
@ -1,11 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# entrypoint.sh
|
# entrypoint.sh
|
||||||
|
|
||||||
source ./.venv/bin/activate
|
source /opt/app/.venv/bin/activate
|
||||||
|
|
||||||
PYTHONPATH="$PYTHONPATH:./dependencytrack-client"
|
PYTHONPATH="$PYTHONPATH:/opt/app/dependencytrack-client"
|
||||||
PYTHONPATH="$PYTHONPATH:./defectdojo-client"
|
PYTHONPATH="$PYTHONPATH:/opt/app/defectdojo-client"
|
||||||
export PYTHONPATH
|
export PYTHONPATH
|
||||||
|
|
||||||
exec python sbom-dt-dd.py "$@"
|
exec python /opt/app/sbom-dt-dd.py "$@"
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
@ -82,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
|
||||||
@ -95,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 --------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
@ -106,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 ------------
|
||||||
|
Reference in New Issue
Block a user