47 Commits
1.1.10 ... main

Author SHA1 Message Date
c63eb169b1 changes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-21 18:53:28 +02:00
b207b5a186 fix 3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-07-30 18:54:52 +02:00
6abd8dcae9 fix 2
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-30 17:58:46 +02:00
037b3e64eb fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-07-30 17:51:22 +02:00
c31679632b add deployment
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-07-30 17:46:04 +02:00
57e9940b3a steps with depends_on
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-07-30 17:05:20 +02:00
136ec74f06 pipeline instead of steps 2025-07-30 16:59:53 +02:00
955937dea7 code styling
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-30 09:08:33 +02:00
98035fe59c fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-16 23:54:04 +02:00
7ddb3c153e form for upload, fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-15 17:24:34 +02:00
677e34f1f3 form for upload 2025-07-15 17:16:14 +02:00
2a2a1316e1 tune error messages, fix 2025-07-15 16:31:56 +02:00
f2d6178304 tune error messages 2025-07-15 16:28:01 +02:00
7236c35ef9 fix paths, fix 2025-07-15 16:01:09 +02:00
b2cf3fe4c7 fix paths 2025-07-15 15:59:21 +02:00
a56119379a add forgotten module 2025-07-15 15:48:46 +02:00
bd368822aa fix paths in api 2025-07-15 15:45:45 +02:00
1cb9451c47 fix shell in entrypoint script of server 2025-07-15 15:40:14 +02:00
5eedb7c523 fix image name confusion, fix 2025-07-15 15:36:32 +02:00
294f30eb38 fix image name confusion
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-15 15:32:53 +02:00
5af202469c fix start script, add port 2025-07-15 15:26:10 +02:00
81bd403069 fix start script 2025-07-15 15:20:26 +02:00
93222237ee adjust ci rules, fix 2025-07-15 15:18:46 +02:00
d5bda1c2d4 adjust ci rules 2025-07-15 15:16:54 +02:00
b430afcfef add deploy stage, fix 3 2025-07-15 15:10:24 +02:00
3ce0b0a4cf add deploy stage, fix 2 2025-07-15 15:09:50 +02:00
c88a74daa3 add deploy stage, fix 2025-07-15 15:09:05 +02:00
10d14d87fb add deploy stage
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-15 15:07:53 +02:00
58795aca81 rename dockerfiles, fix 2 2025-07-15 14:45:32 +02:00
13271a6d5e rename dockerfiles, fix 2025-07-15 14:44:18 +02:00
5a9493fe32 rename dockerfiles
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-15 14:42:53 +02:00
708b99852f add second dockerfile, add ci snippet
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-15 14:40:40 +02:00
e15973db53 add second dockerfile 2025-07-15 14:40:06 +02:00
b2db5b35ad prepare second dockerfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-15 14:33:07 +02:00
b21bd408f7 there is still an error
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-14 23:13:30 +02:00
e1aa900f4d works
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-14 22:47:08 +02:00
91dd245318 add server
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-07-14 22:06:16 +02:00
921a784fc0 add webservice boilerplate snippet
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-14 17:58:43 +02:00
43678c69fb hidden imports, 9 2025-07-14 16:55:56 +02:00
4577f8f0a5 hidden imports, 8 2025-07-14 16:49:32 +02:00
4dd3e9e799 hidden imports, 7 2025-07-14 16:40:35 +02:00
46ce0e1d54 hidden imports, 6 2025-07-14 16:37:16 +02:00
07b5a2a512 hidden imports, 5 2025-07-14 16:32:22 +02:00
d30abf3d0c hidden imports, 4 2025-07-14 16:28:23 +02:00
f8061aaa7a hidden imports, 3 2025-07-14 16:23:01 +02:00
e1cce96308 hidden imports, 2 2025-07-14 16:18:08 +02:00
3bd9882beb hidden imports
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-07-14 16:13:18 +02:00
18 changed files with 995 additions and 307 deletions

View File

@@ -1,10 +1,11 @@
stages:
- generate-api-clients
- build
- deploy
variables:
REGISTRY: devnexus.krohne.com:18079/repository/docker-krohne
IMAGE_NAME: $REGISTRY/$CI_PROJECT_NAME
IMAGE_NAME_PREFIX: $REGISTRY/$CI_PROJECT_NAME
DTRACK_API_URL: https://dtrack-api-rd.krohne.com
DEFECTDOJO_API_URL: https://defectdojo-rd.krohne.com
KROHNE_CA_URL: https://devwiki.krohnegroup.com/lib/exe/fetch.php?media=krohne-ca.crt
@@ -68,7 +69,7 @@ generate-defectdojo-api:
-o defectdojo-client \
--package-name defectdojo_api
dockerize:
.dockerize:
stage: build
image: devnexus.krohne.com:18079/repository/docker-krohne/krohnedockerbash:0.5
tags:
@@ -76,42 +77,82 @@ dockerize:
- docker
- bash
rules:
- if: '$CI_COMMIT_TAG'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "production_deployment"'
script:
- IMAGE_NAME=$IMAGE_NAME_PREFIX"-"$IMAGE_NAME_SUFFIX
- 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:latest-$CI_COMMIT_BRANCH
--tag $IMAGE_NAME:$CI_COMMIT_SHA
--tag $IMAGE_NAME:$CI_COMMIT_TAG
-f $DOCKERFILE
.
- docker login -u $NEXUS_USER -p $NEXUS_PASSWORD $REGISTRY
- docker push $IMAGE_NAME:latest
- docker push $IMAGE_NAME:latest-$CI_COMMIT_BRANCH
- docker push $IMAGE_NAME:$CI_COMMIT_SHA
- docker push $IMAGE_NAME:$CI_COMMIT_TAG
build-windows-binary:
stage: build
tags:
- windows
- pwsh
- python3.13
rules:
- if: '$CI_COMMIT_TAG'
artifacts:
paths:
- sbom-dt-dd.exe
dockerize-cli:
extends: .dockerize
variables:
IMAGE_NAME_SUFFIX: cli
DOCKERFILE: Dockerfile-cli
dockerize-server:
extends: .dockerize
variables:
IMAGE_NAME_SUFFIX: server
DOCKERFILE: Dockerfile-server
.deploy:
stage: deploy
image: devnexus.krohne.com:18079/repository/docker-krohne/krohnedockerbash:0.5
variables:
GIT_STRATEGY: none
SERVICE: sbom-dd-dt-integrator
script:
- IMAGE_NAME=$IMAGE_NAME_PREFIX"-server"
- VERSION=$CI_COMMIT_SHA
- CONTAINER_NAME=$SERVICE"-"$INSTANCE_SPECIFIER
- SERVICE_VOLUME=$SERVICE"-"$INSTANCE_SPECIFIER"-data"
- docker volume inspect $SERVICE_VOLUME || docker volume create $SERVICE_VOLUME
- docker stop $CONTAINER_NAME || echo "$CONTAINER_NAME not running, anyway okay"
- docker rm $CONTAINER_NAME || echo "$CONTAINER_NAME not running, anyway okay"
- docker login -u $NEXUS_USER -p $NEXUS_PASSWORD $REGISTRY
- docker pull $IMAGE_NAME:$VERSION
- |
cd src
mv ..\dependencytrack-client .
mv ..\defectdojo-client .
& 'C:\Program Files\Python313\python.exe' -m venv venv
.\venv\Scripts\pip.exe install --upgrade pip
.\venv\Scripts\pip.exe install -r requirements.txt
.\venv\Scripts\pip.exe install -r dependencytrack-client\requirements.txt
.\venv\Scripts\pip.exe install -r defectdojo-client\requirements.txt
.\venv\Scripts\pip.exe install pyinstaller
.\venv\Scripts\pyinstaller.exe --onefile --add-data "dependencytrack-client;dependencytrack-client" --add-data "defectdojo-client;defectdojo-client" sbom-dt-dd.py
mv dist\sbom-dt-dd.exe ..
cat - > /start-scripts/${CONTAINER_NAME}.sh << EOT
docker run \
-d \
--restart always \
-p 4701:8000 \
--name $CONTAINER_NAME \
-e DTRACK_API_URL=$DTRACK_API_URL \
-e DTRACK_TOKEN=$DTRACK_TOKEN \
-e DEFECTDOJO_URL=$DEFECTDOJO_URL \
-e DEFECTDOJO_TOKEN=$DEFECTDOJO_TOKEN \
$IMAGE_NAME:$VERSION
EOT
- chmod 755 /start-scripts/${CONTAINER_NAME}.sh
- /start-scripts/${CONTAINER_NAME}.sh
deploy-test:
extends: .deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
tags:
- test-deployment-de01rdtst01
variables:
INSTANCE_SPECIFIER: test
environment:
name: test
deploy-dev:
extends: .deploy
rules:
- if: '$CI_COMMIT_BRANCH == "production_deployment"'
tags:
- for-common-services-prod-deployment-only
variables:
INSTANCE_SPECIFIER: prod
environment:
name: prod

View File

@@ -35,27 +35,29 @@ steps:
when:
- event: [ push, tag ]
build:
build-cli:
depends_on: [generate-dtrack-api, generate-defectdojo]
image: plugins/kaniko
settings:
repo: ${FORGE_NAME}/${CI_REPO}
registry:
from_secret: container_registry
tags:
- latest
- ${CI_COMMIT_SHA}
- cli-latest
- cli-${CI_COMMIT_SHA}
username:
from_secret: container_registry_username
password:
from_secret: container_registry_password
dockerfile: Dockerfile
dockerfile: Dockerfile-cli
when:
- event: [ push ]
build-for-quay:
build-cli-for-quay:
depends_on: [generate-dtrack-api, generate-defectdojo]
image: plugins/kaniko
settings:
repo: quay.io/wollud1969/${CI_REPO_NAME}
repo: quay.io/wollud1969/${CI_REPO_NAME}-cli
registry: quay.io
tags:
- latest
@@ -64,8 +66,57 @@ steps:
from_secret: quay_username
password:
from_secret: quay_password
dockerfile: Dockerfile
dockerfile: Dockerfile-cli
when:
- event: [tag]
build-server:
depends_on: [generate-dtrack-api, generate-defectdojo]
image: plugins/kaniko
settings:
repo: ${FORGE_NAME}/${CI_REPO}
registry:
from_secret: container_registry
tags:
- server-latest
- server-${CI_COMMIT_TAG}
username:
from_secret: container_registry_username
password:
from_secret: container_registry_password
dockerfile: Dockerfile-server
when:
- event: [ tag ]
build-server-for-quay:
depends_on: [generate-dtrack-api, generate-defectdojo]
image: plugins/kaniko
settings:
repo: quay.io/wollud1969/${CI_REPO_NAME}-server
registry: quay.io
tags:
- latest
- ${CI_COMMIT_TAG}
username:
from_secret: quay_username
password:
from_secret: quay_password
dockerfile: Dockerfile-server
when:
- event: [tag]
deploy:
image: portainer/kubectl-shell:latest
depends_on: [build-server]
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
commands:
- export IMAGE_TAG=$CI_COMMIT_TAG
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- ./deployment/deploy.sh
when:
- event: [tag]

View File

@@ -31,9 +31,10 @@ USER user
WORKDIR $APP_DIR
COPY src/requirements.txt .
COPY src/sbom-dt-dd.py .
COPY src/sbom_dt_dd.py .
COPY src/sbom_dt_dd_cli.py .
COPY src/converter.py .
COPY src/entrypoint.sh .
COPY src/entrypoint-cli.sh .
COPY dependencytrack-client/ ./dependencytrack-client
COPY defectdojo-client/ ./defectdojo-client
@@ -44,7 +45,7 @@ RUN \
pip install -r dependencytrack-client/requirements.txt &&\
pip install -r defectdojo-client/requirements.txt
ENTRYPOINT [ "./entrypoint.sh" ]
ENTRYPOINT [ "./entrypoint-cli.sh" ]

52
Dockerfile-server Normal file
View File

@@ -0,0 +1,52 @@
FROM python:3.12.10-alpine3.22
ENV DTRACK_API_URL=""
ENV DTRACK_TOKEN=""
ENV DEFECTDOJO_URL=""
ENV DEFECTDOJO_TOKEN=""
ARG APP_DIR=/opt/app
ARG ADDITIONAL_CA_URL="x"
ARG ADDITIONAL_CA_CHECKSUM="y"
RUN \
set -e &&\
adduser -s /bin/sh -D user &&\
mkdir -p $APP_DIR &&\
chown user:user $APP_DIR &&\
echo $ADDITIONAL_CA_URL &&\
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; \
echo "$ADDITIONAL_CA_CHECKSUM custom-ca.crt" | md5sum -c; \
/usr/sbin/update-ca-certificates; \
echo "custom ca added"; \
else \
echo "no additional ca"; \
fi
USER user
WORKDIR $APP_DIR
COPY src/requirements.txt .
COPY src/sbom_dt_dd.py .
COPY src/sbom_dt_dd_api.py .
COPY src/converter.py .
COPY src/entrypoint-server.sh .
COPY dependencytrack-client/ ./dependencytrack-client
COPY defectdojo-client/ ./defectdojo-client
RUN \
python -m venv .venv &&\
. ./.venv/bin/activate &&\
pip install -r requirements.txt &&\
pip install -r dependencytrack-client/requirements.txt &&\
pip install -r defectdojo-client/requirements.txt
EXPOSE 8000
ENTRYPOINT [ "./entrypoint-server.sh" ]

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dtrack-defectdojo-automation-server
labels:
app: dtrack-defectdojo-automation-server
spec:
replicas: 1
selector:
matchLabels:
app: dtrack-defectdojo-automation-server
template:
metadata:
labels:
app: dtrack-defectdojo-automation-server
spec:
containers:
- name: dtrack-defectdojo-automation-server
image: %IMAGE%
envFrom:
- secretRef:
name: dtrack-defectdojo-automation-server
ports:
- containerPort: 8000
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: dtrack-defectdojo-automation-server
spec:
type: ClusterIP
selector:
app: dtrack-defectdojo-automation-server
ports:
- name: http
targetPort: 8000
port: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dtrack-defectdojo-automation-server
spec:
tls:
- hosts:
- webservices.hottis.de
secretName: webservices-cert
rules:
- host: webservices.hottis.de
http:
paths:
- path: /sbom-integrator/v1/
pathType: Prefix
backend:
service:
name: dtrack-defectdojo-automation-server
port:
number: 80

35
deployment/deploy.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
if [ "$IMAGE_TAG" == "" ]; then
echo "Make sure IMAGE_TAG is set"
exit 1
fi
IMAGE_NAME=gitea.hottis.de/wn/dtrack-defectdojo-automation
NAMESPACE=webservices
DEPLOYMENT_DIR=$PWD/deployment
pushd $DEPLOYMENT_DIR >/dev/null
kubectl create namespace $NAMESPACE \
--dry-run=client \
-o yaml |
kubectl -f - apply
kubectl create secret generic dtrack-defectdojo-automation-server \
--dry-run=client \
-o yaml \
--save-config \
--from-literal=DTRACK_API_URL="" \
--from-literal=DTRACK_TOKEN="" \
--from-literal=DEFECTDOJO_URL="" \
--from-literal=DEFECTDOJO_TOKEN="" |
kubectl apply -n $NAMESPACE -f -
cat $DEPLOYMENT_DIR/deploy-yml.tmpl |
sed -e 's,%IMAGE%,'$IMAGE_NAME':server-'$IMAGE_TAG','g |
kubectl apply -f - -n $NAMESPACE
popd >/dev/null

33
snippets/websrv/main.py Normal file
View File

@@ -0,0 +1,33 @@
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI(
title="My FastAPI App",
version="1.0.0",
description="A simple FastAPI example with uvicorn and gunicorn."
)
@app.get("/hello")
async def say_hello(name: str):
"""
Returns a friendly greeting.
---
parameters:
- name: name
in: query
required: true
schema:
type: string
responses:
200:
description: Successful Response
content:
application/json:
schema:
type: object
properties:
message:
type: string
"""
return JSONResponse(content={"message": f"Hello, {name}!"})

View File

@@ -0,0 +1,3 @@
fastapi==0.116.1
gunicorn==23.0.0
uvicorn==0.35.0

4
snippets/websrv/server.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
./.venv/bin/gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000

View File

@@ -47,7 +47,7 @@ def __converterClassifierToComponentType(classifier):
def minimalSbomFormatConverter(minimalSbom, classifier):
def minimalSbomFormatConverter(minimalSbom):
logger.info(f"Minimal input: {minimalSbom}")
lc_factory = LicenseFactory()

View File

@@ -7,5 +7,4 @@ PYTHONPATH="$PYTHONPATH:/opt/app/dependencytrack-client"
PYTHONPATH="$PYTHONPATH:/opt/app/defectdojo-client"
export PYTHONPATH
exec python /opt/app/sbom-dt-dd.py "$@"
exec python /opt/app/sbom_dt_dd_cli.py "$@"

9
src/entrypoint-server.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
source /opt/app/.venv/bin/activate
PYTHONPATH="$PYTHONPATH:/opt/app/dependencytrack-client"
PYTHONPATH="$PYTHONPATH:/opt/app/defectdojo-client"
export PYTHONPATH
gunicorn sbom_dt_dd_api:app -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000

View File

@@ -1,6 +1,6 @@
#!/bin/bash
set -e
set -ex
. ./ENV
@@ -45,3 +45,5 @@ python3 -m venv .venv
pip install -r requirements.txt
pip install -r $LOCALLBIS/dependencytrack-client/requirements.txt
pip install -r $LOCALLBIS/defectdojo-client/requirements.txt

View File

@@ -2,4 +2,7 @@ regex==2024.11.6
loguru==0.7.3
PyYAML==6.0.2
cyclonedx-python-lib==10.4.1
fastapi==0.116.1
gunicorn==23.0.0
uvicorn==0.35.0
python-multipart==0.0.20

View File

@@ -1,263 +0,0 @@
import os
from loguru import logger
import argparse
import subprocess
import json
import datetime
from dateutil.relativedelta import relativedelta
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'defectdojo-client'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'dependencytrack-client'))
import defectdojo_api
from defectdojo_api.rest import ApiException as DefectDojoApiException
import dependencytrack_api
from dependencytrack_api.rest import ApiException as DependencyTrackApiException
from converter import minimalSbomFormatConverter
class MyLocalException(Exception): pass
def executeApiCall(apiClient, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams=[]):
try:
logger.info(f"Calling {ApiClass=}.{EndpointMethod=} with {RequestClass=})")
if VERBOSE:
logger.debug(f"{additionalParams=}, {requestParams=}")
instance = ApiClass(apiClient)
if RequestClass:
request = RequestClass(**requestParams)
response = EndpointMethod(instance, *additionalParams, request)
else:
response = EndpointMethod(instance, *additionalParams)
logger.info(f"Response is {response}")
return response
except Exception as e:
logger.error(f"Caught error {e} with {str(e)}")
raise MyLocalException(e)
def generateSBOM(target='.', name='dummyName', version='0.0.0'):
try:
result = subprocess.run(
["syft", "scan", target, "-o", "cyclonedx-json", "--source-name", name, "--source-version", version],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
sbom = json.loads(result.stdout)
return sbom
except subprocess.CalledProcessError as e:
logger.error(f"SBOM scanner failed: {e.stderr}")
raise MyLocalException(e)
# ---- main starts here with preparation of config -----------------------------------------------------------------------
try:
DTRACK_API_URL = os.environ["DTRACK_API_URL"]
DTRACK_TOKEN = os.environ["DTRACK_TOKEN"]
DEFECTDOJO_URL = os.environ["DEFECTDOJO_URL"]
DEFECTDOJO_TOKEN = os.environ["DEFECTDOJO_TOKEN"]
except KeyError as e:
raise Exception(f"Env variable {e} is shall be set")
parser = argparse.ArgumentParser(description='sbom-dt-dd glue logic')
parser.add_argument('--name', '-n',
help='Project Name',
required=False,
default=''),
parser.add_argument('--version', '-v',
help='Project Version',
required=False,
default='')
parser.add_argument('--description', '-d',
help='Project Description',
required=False,
default='')
parser.add_argument('--type', '-t',
help='Product Type from DefectDojo',
type=int,
required=True)
parser.add_argument('--classifier', '-c',
help='Project Classifier from DependencyTrack',
choices=['APPLICATION', 'FRAMEWORK', 'LIBRARY', 'CONTAINER', 'OPERATING_SYSTEM', 'DEVICE',
'FIRMWARE', 'FILE', 'PLATFORM', 'DEVICE_DRIVER', 'MACHINE_LEARNING_MODEL', 'DATA'],
required=False,
default='')
parser.add_argument('--uploadsbom', '-U',
help='Upload a already existing SBOM instead of generating it. Give the SBOM file at -F instead of a target',
required=False,
action='store_true',
default=False)
parser.add_argument('--sbomfile', '-F',
help='Filename of existing SBOM file to upload, use together with -U, do not use together with -T',
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('--overwritemetadata', '-O',
help='Overwrite name, version, description and classifier with data from minimal SBOM',
action='store_true',
default=False)
parser.add_argument('--target', '-T',
help='Target to scan, either path name for sources or docker image tag',
required=False)
parser.add_argument('--reimport', '-R',
help='Import the SBOM for an existing project/product once again',
required=False,
action='store_true',
default=False)
parser.add_argument('--verbose', '-V',
help='A lot of debug output',
required=False,
action='store_true',
default=False)
args = parser.parse_args()
projectName = args.name
projectVersion = args.version
projectDescription = args.description
productType = args.type
projectClassifier = args.classifier
reImport = args.reimport
uploadSbomFlag = args.uploadsbom
if uploadSbomFlag:
sbomFileName = args.sbomfile
minimalSbomFormat = args.minimalsbomformat
else:
target = args.target
if minimalSbomFormat:
overwriteMetadata = args.overwritemetadata
if not overwriteMetadata and not (projectName and projectVersion and projectClassifier and projectDescription):
raise MyLocalException("If overwriteMetadata is not selected, projectName, projectVersion, projectClassifier and projectDescription must be set.")
VERBOSE = args.verbose
# ---- main starts here --------------------------------------------------------------------------------------------------
if uploadSbomFlag:
# ------- read uploaded SBOM -------------
logger.info(f"Reading SBOM from file {sbomFileName}")
with open(sbomFileName, 'r') as sbomFile:
sbom = sbomFile.read()
logger.info("SBOM file read.")
if minimalSbomFormat:
logger.info("Start converting from minimal format into cyclonedx")
(sbom, nameFromMinimalSbom, versionFromMinimalSbom, classifierFromMinimalSbom, descriptionFromMinimalSbom) = minimalSbomFormatConverter(sbom, projectClassifier)
logger.info("Converted")
if overwriteMetadata:
projectName = nameFromMinimalSbom
projectVersion = versionFromMinimalSbom
projectClassifier = classifierFromMinimalSbom
projectDescription = descriptionFromMinimalSbom
logger.info("Done.")
else:
# ------- generate SBOM ------------
logger.info(f"Generating SBOM for {target}")
sbomJson = generateSBOM(target, projectName, projectVersion)
sbom = json.dumps(sbomJson)
logger.info("Done.")
# ------- create product and engagement in DefectDojo -------
if not reImport:
# in case of a reimport no modification on DefectDojo are required
defectdojo_configuration = defectdojo_api.Configuration(
host = DEFECTDOJO_URL
)
defectdojo_configuration.api_key['tokenAuth'] = DEFECTDOJO_TOKEN
defectdojo_configuration.api_key_prefix['tokenAuth'] = 'Token'
with defectdojo_api.ApiClient(defectdojo_configuration) as defectdojo_api_client:
print("Create product in DefectDojo")
productName = f"{projectName}:{projectVersion}"
product_response = \
executeApiCall(
defectdojo_api_client,
defectdojo_api.ProductsApi,
defectdojo_api.ProductsApi.products_create,
defectdojo_api.ProductRequest,
{ 'name': productName, 'description': projectDescription, 'prod_type': productType },
[]
)
product_id = product_response.id
print(f"{product_id=}")
print("Create engagement in DefectDojo")
start_time = datetime.date.today()
end_time = start_time + relativedelta(years=10)
engagementName = f"{productName} DTrack Link"
engagement_response = \
executeApiCall(
defectdojo_api_client,
defectdojo_api.EngagementsApi,
defectdojo_api.EngagementsApi.engagements_create,
defectdojo_api.EngagementRequest,
{ 'name': engagementName, 'target_start': start_time, 'target_end': end_time, 'status': 'In Progress', 'product': product_id },
[]
)
engagement_id = engagement_response.id
print(f"{engagement_id=}")
# ------- create project in DependencyTrack, connect project to engagement in DefectDojo, upload SBOM --------
dependencytrack_configuration = dependencytrack_api.Configuration(
host = f"{DTRACK_API_URL}/api"
)
dependencytrack_configuration.debug = False
dependencytrack_configuration.api_key['ApiKeyAuth'] = DTRACK_TOKEN
with dependencytrack_api.ApiClient(dependencytrack_configuration) as dependencytrack_api_client:
if not reImport:
# in case of a reimport it is not necessary to create the project
project_response = \
executeApiCall(
dependencytrack_api_client,
dependencytrack_api.ProjectApi,
dependencytrack_api.ProjectApi.create_project,
dependencytrack_api.Project,
{ 'name': projectName, 'version': projectVersion, 'classifier': projectClassifier, 'uuid': "", 'last_bom_import': 0 },
[]
)
project_uuid = project_response.uuid
print(f"{project_uuid=}")
properties = [
{ 'group_name': "integrations", 'property_name': "defectdojo.engagementId",
'property_value': str(engagement_id), 'property_type': "STRING" },
{ 'group_name': "integrations", 'property_name': "defectdojo.doNotReactivate",
'property_value': "true", 'property_type': "BOOLEAN" },
{ 'group_name': "integrations", 'property_name': "defectdojo.reimport",
'property_value': "true", 'property_type': "BOOLEAN" }
]
for property in properties:
executeApiCall(
dependencytrack_api_client,
dependencytrack_api.ProjectPropertyApi,
dependencytrack_api.ProjectPropertyApi.create_property1,
dependencytrack_api.ProjectProperty,
property,
[ project_uuid ]
)
bom_response = \
executeApiCall(
dependencytrack_api_client,
dependencytrack_api.BomApi,
dependencytrack_api.BomApi.upload_bom,
None,
None,
[ None, False, projectName, projectVersion, None, None, None, None, True, sbom ]
)

242
src/sbom_dt_dd.py Normal file
View File

@@ -0,0 +1,242 @@
import os
import sys
import subprocess
from loguru import logger
import json
import datetime
from dateutil.relativedelta import relativedelta
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "defectdojo-client"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "dependencytrack-client"))
import defectdojo_api
from defectdojo_api.rest import ApiException as DefectDojoApiException
import dependencytrack_api
from dependencytrack_api.rest import ApiException as DependencyTrackApiException
class ApiException(Exception):
def __init__(self, cause):
self.cause = cause
self.status = cause.status
self.reason = cause.reason
self.body = cause.body
self.data = cause.data
self.headers = cause.headers
class ApiCallExecutor:
def __init__(self, verbose):
self.verbose = verbose
def innerExecuteApiCall(
self, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams
):
logger.info(f"Calling {ApiClass=}.{EndpointMethod=} with {RequestClass=})")
if self.verbose:
logger.debug(f"{additionalParams=}, {requestParams=}")
instance = ApiClass(self)
if RequestClass:
request = RequestClass(**requestParams)
response = EndpointMethod(instance, *additionalParams, request)
else:
response = EndpointMethod(instance, *additionalParams)
logger.info(f"Response is {response}")
return response
class DefectDojoApiClient(defectdojo_api.ApiClient, ApiCallExecutor):
def __init__(self, config, verbose):
defectdojo_api.ApiClient.__init__(self, config)
ApiCallExecutor.__init__(self, verbose)
def executeApiCall(
self, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams
):
try:
return self.innerExecuteApiCall(
ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams
)
except defectdojo_api.exceptions.ApiException as e:
raise ApiException(e)
class DependencyTrackApiClient(dependencytrack_api.ApiClient, ApiCallExecutor):
def __init__(self, config, verbose):
dependencytrack_api.ApiClient.__init__(self, config)
ApiCallExecutor.__init__(self, verbose)
def executeApiCall(
self, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams
):
try:
return self.innerExecuteApiCall(
ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams
)
except dependencytrack_api.exceptions.ApiException as e:
raise ApiException(e)
def generateSBOM(target=".", name="dummyName", version="0.0.0"):
try:
result = subprocess.run(
[
"syft",
"scan",
target,
"-o",
"cyclonedx-json@1.5",
"--source-name",
name,
"--source-version",
version,
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
sbom = json.loads(result.stdout)
return sbom
except subprocess.CalledProcessError as e:
logger.error(f"SBOM scanner failed: {e.stderr}")
raise MyLocalException(e)
def loadToDTrackAndDefectDojo(
config,
projectName,
projectVersion,
projectClassifier,
projectDescription,
productType,
sbom,
reImport,
):
# ------- create product and engagement in DefectDojo -------
if not reImport:
# in case of a reimport no modification on DefectDojo are required
defectdojo_configuration = defectdojo_api.Configuration(
host=config["DEFECTDOJO_URL"]
)
defectdojo_configuration.api_key["tokenAuth"] = config["DEFECTDOJO_TOKEN"]
defectdojo_configuration.api_key_prefix["tokenAuth"] = "Token"
with DefectDojoApiClient(defectdojo_configuration, config["VERBOSE"]) as client:
print("Create product in DefectDojo")
productName = f"{projectName}:{projectVersion}"
product_response = client.executeApiCall(
defectdojo_api.ProductsApi,
defectdojo_api.ProductsApi.products_create,
defectdojo_api.ProductRequest,
{
"name": productName,
"description": projectDescription,
"prod_type": productType,
},
[],
)
product_id = product_response.id
print(f"{product_id=}")
print("Create engagement in DefectDojo")
start_time = datetime.date.today()
end_time = start_time + relativedelta(years=10)
engagementName = f"{productName} DTrack Link"
engagement_response = client.executeApiCall(
defectdojo_api.EngagementsApi,
defectdojo_api.EngagementsApi.engagements_create,
defectdojo_api.EngagementRequest,
{
"name": engagementName,
"target_start": start_time,
"target_end": end_time,
"status": "In Progress",
"product": product_id,
},
[],
)
engagement_id = engagement_response.id
print(f"{engagement_id=}")
# ------- create project in DependencyTrack, connect project to engagement in DefectDojo, upload SBOM --------
dependencytrack_configuration = dependencytrack_api.Configuration(
host=f"{config['DTRACK_API_URL']}/api"
)
dependencytrack_configuration.debug = False
dependencytrack_configuration.api_key["ApiKeyAuth"] = config["DTRACK_TOKEN"]
with DependencyTrackApiClient(
dependencytrack_configuration, config["VERBOSE"]
) as client:
if not reImport:
# in case of a reimport it is not necessary to create the project
project_response = client.executeApiCall(
dependencytrack_api.ProjectApi,
dependencytrack_api.ProjectApi.create_project,
dependencytrack_api.Project,
{
"name": projectName,
"version": projectVersion,
"classifier": projectClassifier,
"uuid": "",
"last_bom_import": 0,
},
[],
)
project_uuid = project_response.uuid
print(f"{project_uuid=}")
properties = [
{
"group_name": "integrations",
"property_name": "defectdojo.engagementId",
"property_value": str(engagement_id),
"property_type": "STRING",
},
{
"group_name": "integrations",
"property_name": "defectdojo.doNotReactivate",
"property_value": "true",
"property_type": "BOOLEAN",
},
{
"group_name": "integrations",
"property_name": "defectdojo.reimport",
"property_value": "true",
"property_type": "BOOLEAN",
},
]
for property in properties:
client.executeApiCall(
dependencytrack_api.ProjectPropertyApi,
dependencytrack_api.ProjectPropertyApi.create_property1,
dependencytrack_api.ProjectProperty,
property,
[project_uuid],
)
bom_response = client.executeApiCall(
dependencytrack_api.BomApi,
dependencytrack_api.BomApi.upload_bom,
None,
None,
[
None,
False,
projectName,
projectVersion,
None,
None,
None,
None,
True,
sbom,
],
)

240
src/sbom_dt_dd_api.py Normal file
View File

@@ -0,0 +1,240 @@
import os
import json
import yaml
from loguru import logger
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from converter import minimalSbomFormatConverter
from sbom_dt_dd import generateSBOM, loadToDTrackAndDefectDojo, ApiException
app = FastAPI(
title="SBOM DTrack DefectDojo Synchronization API",
version="0.0.1",
description="",
root_path="/sbom-integrator/v1"
)
config = {}
try:
config['DTRACK_API_URL'] = os.environ["DTRACK_API_URL"]
config['DTRACK_TOKEN'] = os.environ["DTRACK_TOKEN"]
config['DEFECTDOJO_URL'] = os.environ["DEFECTDOJO_URL"]
config['DEFECTDOJO_TOKEN'] = os.environ["DEFECTDOJO_TOKEN"]
config['VERBOSE'] = True
except KeyError as e:
raise Exception(f"Env variable {e} is shall be set")
app.state.config = config
@app.get("/upload-form", response_class=HTMLResponse)
async def upload_form(request: Request):
"""
Route serving an HTML page with the upload form
"""
# BY AWARE OF THE HARDCODED ROOT_PATH BELOW
html_content = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Minimal SBOM</title>
</head>
<body>
<h1>Upload Minimal SBOM</h1>
<form id="sbomForm">
<label for="file">Select SBOM file:</label><br>
<input type="file" id="file" name="file" required><br><br>
<label for="reimport">Reimport:</label>
<select name="reimport" id="reimport">
<option value="true">true</option>
<option value="false" selected>false</option>
</select><br><br>
<button type="submit">Upload SBOM</button>
</form>
<div id="result"></div>
<script>
document.getElementById("sbomForm").addEventListener("submit", async function(event) {
event.preventDefault();
let form = document.getElementById("sbomForm");
let formData = new FormData(form);
try {
let response = await fetch("/sbom-integrator/v1/upload-minimal-sbom/", {
method: "POST",
body: formData
});
let resultDiv = document.getElementById("result");
if (response.ok) {
let data = await response.json();
resultDiv.innerHTML = "<p style='color:green;'>Upload successful</p>";
} else {
let errorData = await response.json();
let detail = errorData.detail;
// Dynamisch HTML generieren
let html = "<p style='color:red;'>Upload failed:</p><ul>";
for (const [key, value] of Object.entries(detail)) {
html += "<li style='color:red'><strong>" + key + ":</strong> " + formatValue(value) + "</li>";
}
html += "</ul>";
resultDiv.innerHTML = html;
}
} catch (error) {
console.log(error);
document.getElementById("result").innerHTML = "<p style='color:red;'>Error: " + error + "</p>";
}
});
// Hilfsfunktion für verschachtelte Objekte
function formatValue(value) {
if (typeof value === 'object' && value !== null) {
return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
} else {
return escapeHtml(value);
}
}
function escapeHtml(unsafe) {
if (unsafe === null || unsafe === undefined) {
return '';
}
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content)
@app.post("/upload-minimal-sbom/")
async def uploadMinimalSBOM(
file: UploadFile = File(...),
reimport: bool = Form(...)
):
"""
Endpoint to upload a minimal SBOM definition
"""
try:
sbom = await file.read()
logger.info("Start converting from minimal format into cyclonedx")
(sbom, projectName, projectVersion, projectClassifier, projectDescription) = minimalSbomFormatConverter(sbom)
logger.info("Converted")
loadToDTrackAndDefectDojo(app.state.config, projectName, projectVersion, projectClassifier, projectDescription, 1, sbom, reimport)
logger.info("Done.")
except yaml.scanner.ScannerError as e:
logger.warning(f"uploadMinimalSBOM, yaml ScannerError: {e.context=}, {e.context_mark=}, {e.problem=}, {e.problem_mark=}, {e.note=}")
raise HTTPException(
status_code=400,
detail={
"error": "yaml ScannerError",
"context": e.context,
"context_mark": str(e.context_mark),
"problem": e.problem,
"problem_mark": str(e.problem_mark),
"note": e.note
}
)
except ApiException as e:
logger.warning(f"uploadMinimalSBOM, ApiException: {type(e.cause)=}, {e.status=}, {e.reason=}, {e.body=}")
raise HTTPException(
status_code=e.status,
detail={
"type": str(type(e.cause)),
"reason": e.reason,
"body": e.body,
"data": e.data
}
)
except Exception as e:
logger.warning(f"uploadMinimalSBOM, Exception: {type(e)=}, {str(e)=}")
raise HTTPException(
status_code=500,
detail={
"error": "Exception occurred",
"type": str(type(e)),
"message": str(e)
}
)
return JSONResponse(content={
"message": "Upload successful!"
})
@app.post("/upload-sbom/")
async def uploadSBOM(
file: UploadFile = File(...),
projectName: str = Form(...),
projectVersion: str = Form(...),
projectClassifier: str = Form(...),
projectDescription: str = Form(...),
reimport: bool = Form(...)
):
"""
Endpoint to upload a CycloneDX SBOM
"""
sbom = await file.read()
try:
sbomJson = json.loads(sbom)
sbom = json.dumps(sbomJson)
loadToDTrackAndDefectDojo(app.state.config, projectName, projectVersion, projectClassifier, projectDescription, 1, str(sbom), reimport)
logger.info("Done.")
except json.decoder.JSONDecodeError as e:
logger.warning(f"uploadSBOM, JSONDecodeError: {e.msg=}")
raise HTTPException(
status_code=400,
detail={
"error": "JSON decoding error",
"msg": e.msg,
"doc": e.doc,
"pos": e.pos,
"lineno": e.lineno,
"colno": e.colno
}
)
except ApiException as e:
logger.warning(f"uploadSBOM, ApiException: {type(e.cause)=}, {e.status=}, {e.reason=}, {e.body=}")
raise HTTPException(
status_code=e.status,
detail={
"type": str(type(e.cause)),
"reason": e.reason,
"body": e.body,
"data": e.data
}
)
except Exception as e:
logger.warning(f"uploadSBOM, Exception: {type(e)=}, {str(e)=}")
raise HTTPException(
status_code=500,
detail={
"error": "Exception occurred",
"type": str(type(e)),
"message": str(e)
}
)
return JSONResponse(content={
"message": "Upload successful!"
})

176
src/sbom_dt_dd_cli.py Normal file
View File

@@ -0,0 +1,176 @@
import os
import sys
from loguru import logger
import argparse
import json
from converter import minimalSbomFormatConverter
from sbom_dt_dd import generateSBOM, loadToDTrackAndDefectDojo
class CliException(Exception):
pass
# ---- main starts here with preparation of config -----------------------------------------------------------------------
parser = argparse.ArgumentParser(description="sbom-dt-dd glue logic")
parser.add_argument("--name", "-n", help="Project Name", required=False, default="")
parser.add_argument(
"--version", "-v", help="Project Version", required=False, default=""
)
parser.add_argument(
"--description", "-d", help="Project Description", required=False, default=""
)
parser.add_argument(
"--type", "-t", help="Product Type from DefectDojo", type=int, required=True
)
parser.add_argument(
"--classifier",
"-c",
help="Project Classifier from DependencyTrack",
choices=[
"APPLICATION",
"FRAMEWORK",
"LIBRARY",
"CONTAINER",
"OPERATING_SYSTEM",
"DEVICE",
"FIRMWARE",
"FILE",
"PLATFORM",
"DEVICE_DRIVER",
"MACHINE_LEARNING_MODEL",
"DATA",
],
required=False,
default="",
)
parser.add_argument(
"--uploadsbom",
"-U",
help="Upload a already existing SBOM instead of generating it. Give the SBOM file at -F instead of a target",
required=False,
action="store_true",
default=False,
)
parser.add_argument(
"--sbomfile",
"-F",
help="Filename of existing SBOM file to upload, use together with -U, do not use together with -T",
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(
"--overwritemetadata",
"-O",
help="Overwrite name, version, description and classifier with data from minimal SBOM",
action="store_true",
default=False,
)
parser.add_argument(
"--target",
"-T",
help="Target to scan, either path name for sources or docker image tag",
required=False,
)
parser.add_argument(
"--reimport",
"-R",
help="Import the SBOM for an existing project/product once again",
required=False,
action="store_true",
default=False,
)
parser.add_argument(
"--verbose",
"-V",
help="A lot of debug output",
required=False,
action="store_true",
default=False,
)
args = parser.parse_args()
projectName = args.name
projectVersion = args.version
projectDescription = args.description
productType = args.type
projectClassifier = args.classifier
reImport = args.reimport
uploadSbomFlag = args.uploadsbom
sbomFileName = args.sbomfile
minimalSbomFormat = args.minimalsbomformat
target = args.target
overwriteMetadata = args.overwritemetadata
if not overwriteMetadata and not (
projectName and projectVersion and projectClassifier and projectDescription
):
raise CliException(
"If overwriteMetadata is not selected, projectName, projectVersion, projectClassifier and projectDescription must be set."
)
CONFIG = {}
try:
CONFIG["DTRACK_API_URL"] = os.environ["DTRACK_API_URL"]
CONFIG["DTRACK_TOKEN"] = os.environ["DTRACK_TOKEN"]
CONFIG["DEFECTDOJO_URL"] = os.environ["DEFECTDOJO_URL"]
CONFIG["DEFECTDOJO_TOKEN"] = os.environ["DEFECTDOJO_TOKEN"]
except KeyError as e:
raise CliException(f"Env variable {e} is shall be set") from e
CONFIG["VERBOSE"] = args.verbose
# ---- main starts here --------------------------------------------------------------------------------------------------
if uploadSbomFlag:
# ------- read uploaded SBOM -------------
logger.info(f"Reading SBOM from file {sbomFileName}")
with open(sbomFileName, "r") as sbomFile:
sbom = sbomFile.read()
logger.info("SBOM file read.")
if minimalSbomFormat:
logger.info("Start converting from minimal format into cyclonedx")
(
sbom,
nameFromMinimalSbom,
versionFromMinimalSbom,
classifierFromMinimalSbom,
descriptionFromMinimalSbom,
) = minimalSbomFormatConverter(sbom)
logger.info("Converted")
if overwriteMetadata:
projectName = nameFromMinimalSbom
projectVersion = versionFromMinimalSbom
projectClassifier = classifierFromMinimalSbom
projectDescription = descriptionFromMinimalSbom
logger.info("Done.")
else:
# ------- generate SBOM ------------
logger.info(f"Generating SBOM for {target}")
sbomJson = generateSBOM(target, projectName, projectVersion)
sbom = json.dumps(sbomJson)
logger.info("Done.")
loadToDTrackAndDefectDojo(
CONFIG,
projectName,
projectVersion,
projectClassifier,
projectDescription,
productType,
sbom,
reImport,
)