15 Commits

Author SHA1 Message Date
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
10 changed files with 560 additions and 190 deletions

View File

@@ -79,6 +79,7 @@ generate-defectdojo-api:
rules: rules:
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "production_deployment"' - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "production_deployment"'
script: script:
- IMAGE_NAME=$IMAGE_NAME_PREFIX"-"$IMAGE_NAME_SUFFIX
- docker build --build-arg ADDITIONAL_CA_URL="$KROHNE_CA_URL" - docker build --build-arg ADDITIONAL_CA_URL="$KROHNE_CA_URL"
--build-arg ADDITIONAL_CA_CHECKSUM=$KROHNE_CA_CHECKSUM --build-arg ADDITIONAL_CA_CHECKSUM=$KROHNE_CA_CHECKSUM
--tag $IMAGE_NAME:latest-$CI_COMMIT_BRANCH --tag $IMAGE_NAME:latest-$CI_COMMIT_BRANCH

View File

@@ -35,27 +35,29 @@ steps:
when: when:
- event: [ push, tag ] - event: [ push, tag ]
build: build-cli:
depends_on: [generate-dtrack-api, generate-defectdojo]
image: plugins/kaniko image: plugins/kaniko
settings: settings:
repo: ${FORGE_NAME}/${CI_REPO} repo: ${FORGE_NAME}/${CI_REPO}
registry: registry:
from_secret: container_registry from_secret: container_registry
tags: tags:
- latest - cli-latest
- ${CI_COMMIT_SHA} - cli-${CI_COMMIT_SHA}
username: username:
from_secret: container_registry_username from_secret: container_registry_username
password: password:
from_secret: container_registry_password from_secret: container_registry_password
dockerfile: Dockerfile dockerfile: Dockerfile-cli
when: when:
- event: [ push ] - event: [ push ]
build-for-quay: build-cli-for-quay:
depends_on: [generate-dtrack-api, generate-defectdojo]
image: plugins/kaniko image: plugins/kaniko
settings: settings:
repo: quay.io/wollud1969/${CI_REPO_NAME} repo: quay.io/wollud1969/${CI_REPO_NAME}-cli
registry: quay.io registry: quay.io
tags: tags:
- latest - latest
@@ -64,8 +66,57 @@ steps:
from_secret: quay_username from_secret: quay_username
password: password:
from_secret: quay_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: when:
- event: [tag] - 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

@@ -0,0 +1,57 @@
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%
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

23
deployment/deploy.sh Executable file
View File

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

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
source /opt/app/.venv/bin/activate source /opt/app/.venv/bin/activate

View File

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

View File

@@ -5,3 +5,4 @@ cyclonedx-python-lib==10.4.1
fastapi==0.116.1 fastapi==0.116.1
gunicorn==23.0.0 gunicorn==23.0.0
uvicorn==0.35.0 uvicorn==0.35.0
python-multipart==0.0.20

View File

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

View File

@@ -2,15 +2,17 @@ import os
import json import json
import yaml import yaml
from loguru import logger from loguru import logger
from fastapi import FastAPI, UploadFile, File, Form, HTTPException from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from converter import minimalSbomFormatConverter from converter import minimalSbomFormatConverter
from sbom_dt_dd import generateSBOM, loadToDTrackAndDefectDojo, ApiException from sbom_dt_dd import generateSBOM, loadToDTrackAndDefectDojo, ApiException
app = FastAPI( app = FastAPI(
title="SBOM DTrack DefectDojo Synchronization API", title="SBOM DTrack DefectDojo Synchronization API",
version="0.0.1", version="0.0.1",
description="" description="",
root_path="/sbom-integrator/v1"
) )
config = {} config = {}
@@ -26,31 +28,103 @@ except KeyError as e:
app.state.config = config app.state.config = config
@app.get("/hello") @app.get("/upload-form", response_class=HTMLResponse)
async def say_hello(name: str): async def upload_form(request: Request):
""" """
Returns a friendly greeting. Route serving an HTML page with the upload form
---
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}!"}) # 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>
@app.post("/uploadMinimalSBOM/") <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( async def uploadMinimalSBOM(
file: UploadFile = File(...), file: UploadFile = File(...),
reimport: bool = Form(...) reimport: bool = Form(...)
@@ -69,19 +143,44 @@ async def uploadMinimalSBOM(
logger.info("Done.") logger.info("Done.")
except yaml.scanner.ScannerError as e: except yaml.scanner.ScannerError as e:
logger.warning(f"uploadMinimalSBOM, yaml ScannerError: {e.context=}, {e.context_mark=}, {e.problem=}, {e.problem_mark=}, {e.note=}") logger.warning(f"uploadMinimalSBOM, yaml ScannerError: {e.context=}, {e.context_mark=}, {e.problem=}, {e.problem_mark=}, {e.note=}")
raise HTTPException(status_code=400, detail=f"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: except ApiException as e:
logger.warning(f"uploadMinimalSBOM, ApiException: {e.status=}, {e.reason=}, {e.body=}") logger.warning(f"uploadMinimalSBOM, ApiException: {type(e.cause)=}, {e.status=}, {e.reason=}, {e.body=}")
raise HTTPException(status_code=e.status, detail=f"{e.reason=}, {e.body=}, {e.data=}") 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: except Exception as e:
logger.warning(f"uploadMinimalSBOM, Exception: {type(e)=}, {str(e)=}, {e.msg=}") logger.warning(f"uploadMinimalSBOM, Exception: {type(e)=}, {str(e)=}")
raise HTTPException(status_code=500, detail=f"Exception: {type(e)=}, {str(e)=}, {e.msg=}") raise HTTPException(
status_code=500,
detail={
"error": "Exception occurred",
"type": str(type(e)),
"message": str(e)
}
)
return JSONResponse(content={ return JSONResponse(content={
"message": "Upload successful!" "message": "Upload successful!"
}) })
@app.post("/uploadSBOM/") @app.post("/upload-sbom/")
async def uploadSBOM( async def uploadSBOM(
file: UploadFile = File(...), file: UploadFile = File(...),
projectName: str = Form(...), projectName: str = Form(...),
@@ -103,13 +202,38 @@ async def uploadSBOM(
logger.info("Done.") logger.info("Done.")
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
logger.warning(f"uploadSBOM, JSONDecodeError: {e.msg=}") logger.warning(f"uploadSBOM, JSONDecodeError: {e.msg=}")
raise HTTPException(status_code=400, detail=f"JSON decoding error: {e.msg=}, {e.doc=}, {e.pos=}, {e.lineno=}, {e.colno=}") 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: except ApiException as e:
logger.warning(f"uploadSBOM, ApiException: {e.status=}, {e.reason=}, {e.body=}") logger.warning(f"uploadSBOM, ApiException: {type(e.cause)=}, {e.status=}, {e.reason=}, {e.body=}")
raise HTTPException(status_code=e.status, detail=f"{e.reason=}, {e.body=}, {e.data=}") 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: except Exception as e:
logger.warning(f"uploadSBOM, Exception: {type(e)=}, {str(e)=}, {e.msg=}") logger.warning(f"uploadSBOM, Exception: {type(e)=}, {str(e)=}")
raise HTTPException(status_code=500, detail=f"Exception: {type(e)=}, {str(e)=}, {e.msg=}") raise HTTPException(
status_code=500,
detail={
"error": "Exception occurred",
"type": str(type(e)),
"message": str(e)
}
)
return JSONResponse(content={ return JSONResponse(content={
"message": "Upload successful!" "message": "Upload successful!"

View File

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