Skip to content

Commit 835a3b4

Browse files
committed
feat: allow transform of reports before upload
1 parent 1d555fd commit 835a3b4

8 files changed

Lines changed: 253 additions & 10 deletions

File tree

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ RUN groupadd --gid 1000 app && \
1616

1717
COPY --from=build /app /app
1818

19+
RUN apt-get update -y -qq && \
20+
apt-get install -y -qq --no-install-recommends jq=1.7.1-6+deb13u1 kubectl=1.32.3+ds-2 curl=8.14.1-2+deb13u2 && \
21+
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
22+
1923
COPY src/* /app/
2024

2125
RUN chown -R app:app /app

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ analysis and tracking.
1010

1111
* Monitor Kubernetes for new Trivy vulnerability reports.
1212
* Push vulnerability reports to a configured Defect Dojo instance.
13+
* **Transformation Hook**: Intercept and modify reports (e.g., for custom deduplication or enrichment) before upload.
1314
* Seamless integration with your existing Kubernetes cluster and security workflow.
1415
* Developed using the Pythonic Kopf framework for easy maintenance and extensibility.
1516

@@ -127,6 +128,11 @@ For a local development setup, please take a look at
127128
| `http_proxy` | *(empty)* | HTTP proxy setting (optional). |
128129
| `https_proxy` | *(empty)* | HTTPS proxy setting (optional). |
129130
| `excludedNamespaces` | *(empty)* | List of namespace globs patterns to exclude from processing. Each pattern is converted into a --namespace=!<pattern> CLI argument passed to the operator Deployment. Reports from these namespaces will be ignored (optional). |
131+
| `transformation.enabled` | `false` | Enable the transformation hook to modify reports before upload. |
132+
| `transformation.scriptConfigMap` | *(empty)* | Name of a ConfigMap containing the transformation script. |
133+
| `transformation.scriptFilename` | `transform.py` | Filename of the script within the ConfigMap. |
134+
| `transformation.interpreter` | `python3` | Command used to execute the script (e.g., `python3`, `bash`, `jq`). |
135+
| `transformation.scanType` | `Generic Findings Import` | The DefectDojo scanner type (parser) to use if the transformation is successful. |
130136

131137
### A note on eval
132138

@@ -141,6 +147,45 @@ evaluated and used as the engagement name.
141147
If you set defectDojoEngagementName to `body["report"]["artifact"]["tag"]`,
142148
then the engagement will get the name of the specified image-tag.
143149

150+
## Transformation Hook
151+
152+
The transformation hook allows you to manipulate the report data exactly as you need it before it reaches DefectDojo. Common use cases include:
153+
* Custom deduplication logic.
154+
* Enriching findings with specific labels from the scanned image.
155+
* Filtering out specific findings.
156+
157+
### How it works
158+
The operator serializes the raw Trivy report to a JSON string and pipes it into your script's `stdin`. Your script must write the final modified JSON to `stdout`.
159+
160+
If the script exits with code `0`, the operator uses the modified content and switches the DefectDojo `scan_type` to the one configured in `transformation.scanType`. If the script fails, the operator automatically falls back to sending the original report with the default "Trivy Operator Scan" type.
161+
162+
### Example: Using JQ
163+
To simply remove a specific field from the report, you can use `jq` as your interpreter:
164+
165+
```yaml
166+
transformation:
167+
enabled: true
168+
interpreter: "jq"
169+
scriptConfigMap: "jq-transform-cm"
170+
scriptFilename: "filter.jq"
171+
scanType: "Generic Findings Import"
172+
```
173+
174+
### Example: Custom Shell Script
175+
If you need more complex logic, use a shell script:
176+
177+
```bash
178+
#!/bin/bash
179+
# Read stdin into a variable or file
180+
REPORT=$(cat -)
181+
182+
# Perform transformation (e.g., using jq)
183+
MODIFIED_REPORT=$(echo "$REPORT" | jq '.metrics += {"source": "trivy-operator"}')
184+
185+
# Output back to stdout
186+
echo "$MODIFIED_REPORT"
187+
```
188+
144189
## Metrics
145190

146191
The operator provides a Prometheus metrics endpoint(:9090/metrics), where

agents.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Codebase Overview and Agents
2+
3+
## Project Summary
4+
`trivy-dojo-report-operator` is a Kubernetes operator that automates the export of security reports from the Trivy Operator to DefectDojo. It is built using Python and the Kopf (Kubernetes Operator Pythonic Framework) framework.
5+
6+
## Architecture
7+
8+
The operator acts as a bridge between the Kubernetes cluster's security state (managed by Trivy) and the vulnerability management system (DefectDojo).
9+
10+
### "Agents" (Handlers)
11+
12+
The system consists of the following event handlers (agents) defined in `src/handlers.py`:
13+
14+
1. **Startup Agent (`configure`)**
15+
- **Type:** `@kopf.on.startup()`
16+
- **Responsibility:** Initializes the operator configuration, setting timeouts for watching resources to prevent connection drops, and configuring the persistence storage mechanism (`StatusDiffBaseStorage`) to handle large resource objects efficiently without overloading Kubernetes annotations.
17+
18+
2. **Report Forwarding Agent (`send_to_dojo`)**
19+
- **Type:** `@kopf.on.create(...)` (Dynamic Registration)
20+
- **Trigger:** Creation of Trivy-generated Custom Resources (CRs). The specific resources watched are configurable, but typically include:
21+
- `vulnerabilityreports.aquasecurity.github.io`
22+
- `configauditreports.aquasecurity.github.io`
23+
- `exposedsecretreports.aquasecurity.github.io`
24+
- `infraassessmentreports.aquasecurity.github.io`
25+
- `rbacassessmentreports.aquasecurity.github.io`
26+
- **Responsibility:**
27+
- **Extraction:** Extracts the full manifest of the created report.
28+
- **Transformation:** Converts the Kubernetes object into a JSON-compatible dictionary.
29+
- **Context Resolution:** dynamic evaluation of DefectDojo engagement parameters (Product Type, Product, Environment, Engagement Name) using the logic in `settings.py`.
30+
- **Transmission:** Uploads the report to the DefectDojo API (`/api/v2/import-scan/`) using the `requests` library.
31+
- **Observability:** Records Prometheus metrics, including processing time (`request_processing_seconds`) and request counters (`requests_total`).
32+
33+
## Configuration
34+
35+
The agents are configured via environment variables (loaded in `src/settings.py` and `src/env_vars.py`), controlling:
36+
37+
- **Connectivity:** DefectDojo URL (`DEFECT_DOJO_URL`) and API Key (`DEFECT_DOJO_API_KEY`).
38+
- **Scope:** optional filtering by Kubernetes Label (`LABEL`, `LABEL_VALUE`).
39+
- **Import Logic:**
40+
- `DEFECT_DOJO_ACTIVE`: Whether findings are marked as active.
41+
- `DEFECT_DOJO_VERIFIED`: Whether findings are marked as verified.
42+
- `DEFECT_DOJO_CLOSE_OLD_FINDINGS`: Whether to close old findings in DefectDojo.
43+
- `DEFECT_DOJO_PUSH_TO_JIRA`: Whether to push findings to Jira.
44+
45+
## Deployment
46+
47+
The operator is packaged as a Docker container and deployed via a Helm chart located in the `charts/` directory.

charts/templates/deployment.yaml

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,15 @@ spec:
101101
value: {{ quote .Values.operator.trivyDojoReportOperator.env.http_proxy }}
102102
- name: HTTPS_PROXY
103103
value: {{ quote .Values.operator.trivyDojoReportOperator.env.https_proxy }}
104-
image: {{ .Values.operator.trivyDojoReportOperator.image.repository }}:{{ .Values.operator.trivyDojoReportOperator.image.tag | default .Chart.AppVersion }}
104+
- name: TRANSFORMATION_ENABLED
105+
value: {{ quote .Values.operator.trivyDojoReportOperator.transformation.enabled }}
106+
- name: TRANSFORMATION_SCRIPT_PATH
107+
value: "/scripts/{{ .Values.operator.trivyDojoReportOperator.transformation.scriptFilename }}"
108+
- name: TRANSFORMATION_INTERPRETER
109+
value: {{ quote .Values.operator.trivyDojoReportOperator.transformation.interpreter }}
110+
- name: DEFECT_DOJO_SCAN_TYPE_OVERRIDE
111+
value: {{ quote .Values.operator.trivyDojoReportOperator.transformation.scanType }}
112+
image: {{ printf "%s:%s" .Values.operator.trivyDojoReportOperator.image.repository (.Values.operator.trivyDojoReportOperator.image.tag | default .Chart.AppVersion) | quote }}
105113
livenessProbe:
106114
httpGet:
107115
path: /healthz
@@ -121,20 +129,33 @@ spec:
121129
{{- end }}
122130
securityContext: {{- toYaml .Values.operator.trivyDojoReportOperator.containerSecurityContext
123131
| nindent 10 }}
124-
{{- with .Values.operator.trivyDojoReportOperator.extraVolumeMounts }}
125132
volumeMounts:
133+
- mountPath: /tmp
134+
name: tmp
135+
{{- if .Values.operator.trivyDojoReportOperator.transformation.scriptConfigMap }}
136+
- mountPath: /scripts
137+
name: transformation-scripts
138+
{{- end }}
139+
{{- with .Values.operator.trivyDojoReportOperator.extraVolumeMounts }}
126140
{{- toYaml . | nindent 10 }}
127-
{{- end }}
141+
{{- end }}
128142
{{- with .Values.operator.trivyDojoReportOperator.imagePullSecrets }}
129143
imagePullSecrets:
130144
{{- toYaml . | nindent 8 }}
131145
{{- end }}
132146
securityContext: {{- toYaml .Values.operator.podSecurityContext | nindent 8 }}
133147
# Additional volumes on the output Deployment definition.
134-
{{- with .Values.operator.trivyDojoReportOperator.extraVolumes }}
135148
volumes:
149+
- name: tmp
150+
emptyDir: {}
151+
{{- if .Values.operator.trivyDojoReportOperator.transformation.scriptConfigMap }}
152+
- name: transformation-scripts
153+
configMap:
154+
name: {{ .Values.operator.trivyDojoReportOperator.transformation.scriptConfigMap }}
155+
{{- end }}
156+
{{- with .Values.operator.trivyDojoReportOperator.extraVolumes }}
136157
{{- toYaml . | nindent 8 }}
137-
{{- end }}
158+
{{- end }}
138159
serviceAccountName: {{ include "charts.fullname" . }}-account
139160
{{- with .Values.operator.trivyDojoReportOperator.nodeSelector }}
140161
nodeSelector:

charts/templates/rbac.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,29 @@ rules:
3838
- ""
3939
resources:
4040
- namespaces
41+
- pods
42+
- secrets
43+
- replicationcontrollers
4144
verbs:
4245
- list
4346
- watch
47+
- get
48+
- apiGroups:
49+
- apps
50+
resources:
51+
- deployments
52+
- statefulsets
53+
- daemonsets
54+
- replicasets
55+
verbs:
56+
- get
57+
- apiGroups:
58+
- batch
59+
resources:
60+
- jobs
61+
- cronjobs
62+
verbs:
63+
- get
4464
- apiGroups:
4565
- ""
4666
resources:

charts/values.yaml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,20 @@ operator:
3939
defectDojoCloseOldFindingsProductScope: "false"
4040
defectDojoDeduplicationOnEngagement: "true"
4141
defectDojoDoNotReactivate: "true"
42-
defectDojoEngagementName: engagement
42+
defectDojoEngagementName: "full_object.get('meta_engagement_name', 'Default Engagement')"
43+
defectDojoTags: "[f\"base_image:{full_object.get('meta_base_image', 'unknown')}\", f\"component:{full_object.get('meta_engagement_name', 'unknown')}\"]"
4344
defectDojoEnvName: Development
44-
defectDojoEvalEngagementName: "false"
45+
defectDojoProductTypeName: "Research and Development"
46+
defectDojoProductName: "BARMER CoreMedia"
47+
defectDojoEvalEngagementName: "true"
4548
defectDojoEvalEnvName: "false"
4649
defectDojoEvalProductName: "false"
50+
defectDojoServiceName: ""
51+
defectDojoEvalServiceName: "false"
4752
defectDojoEvalProductTypeName: "false"
4853
defectDojoEvalTestTitle: "false"
54+
defectDojoEvalTags: "true"
4955
defectDojoMinimumSeverity: Info
50-
defectDojoProductName: product
51-
defectDojoProductTypeName: Research and Development
5256
defectDojoPushToJira: "false"
5357
defectDojoTestTitle: Kubernetes
5458
defectDojoVerified: "false"
@@ -59,6 +63,12 @@ operator:
5963
repository: ghcr.io/telekom-mms/docker-trivy-dojo-operator
6064
tag: 0.9.2
6165
imagePullSecrets: []
66+
transformation:
67+
enabled: true
68+
scriptConfigMap: "trivy-dojo-transform-script"
69+
scriptFilename: "wrapper.sh"
70+
interpreter: "bash"
71+
scanType: "Generic Findings Import"
6272
type: ClusterIP
6373
podSecurityContext:
6474
runAsNonRoot: true

src/handlers.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import prometheus_client
44
import requests
55
import settings
6+
import subprocess
7+
import os
68

79
from requests.exceptions import HTTPError
810
from io import BytesIO
@@ -36,6 +38,63 @@ def check_allowed_reports(report: str):
3638
exit(1)
3739

3840

41+
def run_transformation(raw_report: dict, logger) -> dict | None:
42+
"""
43+
Runs the transformation script on the raw report using piping (stdin/stdout).
44+
Returns the transformed report as a dict or None if transformation failed.
45+
"""
46+
try:
47+
# 1. Serialize Input
48+
input_data = json.dumps(raw_report)
49+
50+
# 2. Execute Script via pipe
51+
cmd = [
52+
settings.TRANSFORMATION_INTERPRETER,
53+
settings.TRANSFORMATION_SCRIPT_PATH,
54+
]
55+
logger.info(f"Executing transformation: {' '.join(cmd)}")
56+
57+
result = subprocess.run(
58+
cmd,
59+
input=input_data,
60+
capture_output=True,
61+
text=True,
62+
env=os.environ,
63+
check=True,
64+
)
65+
66+
# 3. Parse Output
67+
if not result.stdout.strip():
68+
logger.error("Transformation script returned empty stdout")
69+
if result.stderr:
70+
logger.error(f"Transformation stderr: {result.stderr}")
71+
return None
72+
73+
transformed_data = json.loads(result.stdout)
74+
if result.stderr:
75+
for line in result.stderr.splitlines():
76+
logger.info(f"Transformation: {line}")
77+
return transformed_data
78+
79+
except subprocess.CalledProcessError as e:
80+
logger.error(f"Transformation script failed with exit code {e.returncode}")
81+
if e.stderr:
82+
logger.error(f"Transformation stderr: {e.stderr}")
83+
if e.stdout:
84+
logger.error(f"Transformation stdout (partial): {e.stdout[:500]}")
85+
return None
86+
except json.JSONDecodeError as e:
87+
logger.error(f"Failed to parse transformation output as JSON: {e}")
88+
if result.stdout:
89+
logger.error(f"Raw output (partial): {result.stdout[:500]}...")
90+
if result.stderr:
91+
logger.error(f"Transformation stderr: {result.stderr}")
92+
return None
93+
except Exception as e:
94+
logger.error(f"Error during transformation: {e}")
95+
return None
96+
97+
3998
@kopf.on.startup()
4099
def configure(settings: kopf.OperatorSettings, **_):
41100
"""
@@ -95,6 +154,17 @@ def send_to_dojo(body, meta, logger, **_):
95154

96155
logger.debug(full_object)
97156

157+
scan_type = "Trivy Operator Scan"
158+
if settings.TRANSFORMATION_ENABLED:
159+
logger.info("Transformation Hook is enabled")
160+
transformed_object = run_transformation(full_object, logger)
161+
if transformed_object:
162+
logger.info("Transformation successful")
163+
full_object = transformed_object
164+
scan_type = settings.DEFECT_DOJO_SCAN_TYPE_OVERRIDE
165+
else:
166+
logger.warning("Transformation failed, falling back to raw report")
167+
98168
_DEFECT_DOJO_ENGAGEMENT_NAME = (
99169
eval(settings.DEFECT_DOJO_ENGAGEMENT_NAME)
100170
if settings.DEFECT_DOJO_EVAL_ENGAGEMENT_NAME
@@ -130,6 +200,16 @@ def send_to_dojo(body, meta, logger, **_):
130200
else settings.DEFECT_DOJO_TEST_TITLE
131201
)
132202

203+
_DEFECT_DOJO_TAGS = (
204+
eval(settings.DEFECT_DOJO_TAGS)
205+
if settings.DEFECT_DOJO_EVAL_TAGS
206+
else (settings.DEFECT_DOJO_TAGS.split(",") if settings.DEFECT_DOJO_TAGS else [])
207+
)
208+
209+
logger.info(f"DefectDojo Config - Engagement: {_DEFECT_DOJO_ENGAGEMENT_NAME}, Test: {_DEFECT_DOJO_TEST_TITLE}, Service: {_DEFECT_DOJO_SERVICE_NAME}")
210+
logger.info(f"Transformation Metadata - base_image: {full_object.get('meta_base_image')}, tag: {full_object.get('meta_tag')}")
211+
212+
133213
# define the vulnerabilityreport as a json-file so DD accepts it
134214
json_string: str = json.dumps(full_object)
135215
json_file: BytesIO = BytesIO(json_string.encode("utf-8"))
@@ -149,14 +229,15 @@ def send_to_dojo(body, meta, logger, **_):
149229
"minimum_severity": settings.DEFECT_DOJO_MINIMUM_SEVERITY,
150230
"auto_create_context": settings.DEFECT_DOJO_AUTO_CREATE_CONTEXT,
151231
"deduplication_on_engagement": settings.DEFECT_DOJO_DEDUPLICATION_ON_ENGAGEMENT,
152-
"scan_type": "Trivy Operator Scan",
232+
"scan_type": scan_type,
153233
"engagement_name": _DEFECT_DOJO_ENGAGEMENT_NAME,
154234
"product_name": _DEFECT_DOJO_PRODUCT_NAME,
155235
"product_type_name": _DEFECT_DOJO_PRODUCT_TYPE_NAME,
156236
"service": _DEFECT_DOJO_SERVICE_NAME,
157237
"environment": _DEFECT_DOJO_ENV_NAME,
158238
"test_title": _DEFECT_DOJO_TEST_TITLE,
159239
"do_not_reactivate": settings.DEFECT_DOJO_DO_NOT_REACTIVATE,
240+
"tags": _DEFECT_DOJO_TAGS,
160241
}
161242

162243
logger.debug(data)

src/settings.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
DEFECT_DOJO_TEST_TITLE: str = os.getenv("DEFECT_DOJO_TEST_TITLE", "Kubernetes")
5151
DEFECT_DOJO_EVAL_TEST_TITLE: bool = get_env_var_bool("DEFECT_DOJO_EVAL_TEST_TITLE")
5252

53+
DEFECT_DOJO_VERSION: str = os.getenv("DEFECT_DOJO_VERSION", "")
54+
DEFECT_DOJO_EVAL_VERSION: bool = get_env_var_bool("DEFECT_DOJO_EVAL_VERSION")
55+
56+
DEFECT_DOJO_TAGS: str = os.getenv("DEFECT_DOJO_TAGS", "")
57+
DEFECT_DOJO_EVAL_TAGS: bool = get_env_var_bool("DEFECT_DOJO_EVAL_TAGS")
58+
5359
DEFECT_DOJO_ENGAGEMENT_NAME: str | None = os.getenv("DEFECT_DOJO_ENGAGEMENT_NAME")
5460
DEFECT_DOJO_EVAL_ENGAGEMENT_NAME: bool = get_env_var_bool(
5561
"DEFECT_DOJO_EVAL_ENGAGEMENT_NAME"
@@ -66,3 +72,12 @@
6672

6773
HTTP_PROXY: str = os.getenv("HTTP_PROXY") or os.getenv("http_proxy")
6874
HTTPS_PROXY: str = os.getenv("HTTPS_PROXY") or os.getenv("https_proxy")
75+
76+
TRANSFORMATION_ENABLED: bool = get_env_var_bool("TRANSFORMATION_ENABLED")
77+
TRANSFORMATION_SCRIPT_PATH: str = os.getenv(
78+
"TRANSFORMATION_SCRIPT_PATH", "/scripts/transform.py"
79+
)
80+
TRANSFORMATION_INTERPRETER: str = os.getenv("TRANSFORMATION_INTERPRETER", "python3")
81+
DEFECT_DOJO_SCAN_TYPE_OVERRIDE: str = os.getenv(
82+
"DEFECT_DOJO_SCAN_TYPE_OVERRIDE", "Generic Findings Import"
83+
)

0 commit comments

Comments
 (0)