diff --git a/helm/templates/clusterworkflowtemplate.yaml b/helm/templates/clusterworkflowtemplate.yaml index 67f4f7a..83dde14 100644 --- a/helm/templates/clusterworkflowtemplate.yaml +++ b/helm/templates/clusterworkflowtemplate.yaml @@ -45,6 +45,33 @@ spec: value: "{{workflow.parameters.working-dir}}" - name: fail-on-cvss value: "{{workflow.parameters.fail-on-cvss}}" + - name: upload-storage + dependencies: + - scan-trufflehog + - scan-semgrep + - scan-kics + - scan-socketdev + - scan-syft-grype + - scan-crossguard + template: upload-storage + - name: upload-defectdojo + dependencies: + - scan-trufflehog + - scan-semgrep + - scan-kics + - scan-socketdev + - scan-syft-grype + - scan-crossguard + template: upload-defectdojo + - name: enforce-policy + dependencies: + - upload-storage + - upload-defectdojo + template: enforce-policy + arguments: + parameters: + - name: fail-on-cvss + value: "{{workflow.parameters.fail-on-cvss}}" - name: sinks-and-enforcement dependencies: - scanners @@ -132,9 +159,6 @@ spec: - name: defectdojo template: scan-crossguard - name: sinks-and-enforcement - metadata: - annotations: - secrets.infisical.com/auto-reload: "true" container: image: alpine:3.20 command: @@ -154,3 +178,9 @@ spec: template: scan-syft-grype - name: scan-crossguard template: scan-crossguard + - name: upload-storage + template: upload-storage + - name: upload-defectdojo + template: upload-defectdojo + - name: enforce-policy + template: enforce-policy diff --git a/helm/templates/enforce-policy.yaml b/helm/templates/enforce-policy.yaml new file mode 100644 index 0000000..3ef46ad --- /dev/null +++ b/helm/templates/enforce-policy.yaml @@ -0,0 +1,88 @@ +{{- if .Values.pipeline.enabled }} +apiVersion: argoproj.io/v1alpha1 +kind: ClusterWorkflowTemplate +metadata: + name: amp-security-pipeline-v1.0.0 +spec: + templates: + - name: enforce-policy + inputs: + parameters: + - name: fail-on-cvss + container: + image: python:3.12-alpine + command: + - sh + - -c + args: + - | + set -eu + python - <<'PY' + import json + import os + import pathlib + import sys + + threshold = float(os.environ["FAIL_ON_CVSS"]) + reports_dir = pathlib.Path("/workspace/reports") + findings = [] + + for report in sorted(reports_dir.iterdir()): + if not report.is_file(): + continue + text = report.read_text(errors="ignore") + if report.suffix == ".sarif": + try: + data = json.loads(text) + except json.JSONDecodeError: + continue + for run in data.get("runs", []): + for result in run.get("results", []): + for fix in result.get("properties", {}).get("security-severity", []): + pass + for level in result.get("properties", {}).values(): + pass + for prop in [result.get("properties", {}), result.get("taxa", [])]: + pass + for region in result.get("locations", []): + pass + sev = result.get("properties", {}).get("security-severity") + if sev is None: + continue + try: + score = float(sev) + except (TypeError, ValueError): + continue + if score >= threshold: + findings.append((report.name, score)) + elif report.suffix == ".json": + try: + data = json.loads(text) + except json.JSONDecodeError: + continue + if isinstance(data, dict): + for item in data.get("findings", data.get("vulnerabilities", [])): + score = item.get("cvss") or item.get("score") + if score is None: + continue + try: + score = float(score) + except (TypeError, ValueError): + continue + if score >= threshold: + findings.append((report.name, score)) + + if findings: + for name, score in findings: + print(f"{name}: CVSS {score} >= {threshold}", file=sys.stderr) + raise SystemExit(1) + + print(f"No findings met or exceeded CVSS {threshold}") + PY + env: + - name: FAIL_ON_CVSS + value: "{{inputs.parameters.fail-on-cvss}}" + volumeMounts: + - name: workspace + mountPath: /workspace +{{- end }} diff --git a/helm/templates/scan-crossguard.yaml b/helm/templates/scan-crossguard.yaml index b3539ec..8088cbc 100644 --- a/helm/templates/scan-crossguard.yaml +++ b/helm/templates/scan-crossguard.yaml @@ -6,24 +6,33 @@ metadata: spec: templates: - name: scan-crossguard - metadata: - annotations: - secrets.infisical.com/auto-reload: "true" - initContainers: - - name: wait-for-infisical - image: alpine:3.20 - command: - - sh - - -c - args: - - until [ -n "${DEFECTDOJO_API_KEY:-}" ]; do sleep 2; done container: - image: alpine:3.20 + image: pulumi/pulumi:3.154.0 + env: + - name: PULUMI_ACCESS_TOKEN + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: PULUMI_ACCESS_TOKEN + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: AWS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: AWS_SECRET_ACCESS_KEY command: - sh - -c args: - - mkdir -p /workspace/reports && echo "stub: defectdojo" > /workspace/reports/crossguard.json + - | + set -eu + mkdir -p /workspace/reports + cd /workspace + pulumi preview --policy-pack ./policy-pack > /workspace/reports/crossguard.json 2>&1 || true volumeMounts: - name: workspace mountPath: /workspace diff --git a/helm/templates/upload-defectdojo.yaml b/helm/templates/upload-defectdojo.yaml new file mode 100644 index 0000000..9504b9a --- /dev/null +++ b/helm/templates/upload-defectdojo.yaml @@ -0,0 +1,66 @@ +{{- if .Values.pipeline.enabled }} +apiVersion: argoproj.io/v1alpha1 +kind: ClusterWorkflowTemplate +metadata: + name: amp-security-pipeline-v1.0.0 +spec: + templates: + - name: upload-defectdojo + container: + image: python:3.12-alpine + env: + - name: DEFECTDOJO_URL + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: DEFECTDOJO_URL + - name: DEFECTDOJO_API_TOKEN + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: DEFECTDOJO_API_TOKEN + command: + - sh + - -c + args: + - | + set -eu + python - <<'PY' + import json + import os + import pathlib + import urllib.request + + base_url = os.environ["DEFECTDOJO_URL"].rstrip("/") + api_token = os.environ["DEFECTDOJO_API_TOKEN"] + product_name = os.environ.get("DEFECTDOJO_PRODUCT_NAME", "agentguard-ci") + scan_map = { + ".sarif": "SARIF", + ".json": "Generic Findings Import", + } + reports_dir = pathlib.Path("/workspace/reports") + for report in sorted(reports_dir.iterdir()): + if not report.is_file(): + continue + scan_type = scan_map.get(report.suffix) + if not scan_type: + continue + req = urllib.request.Request( + f"{base_url}/api/v2/import-scan/", + data=json.dumps({ + "scan_type": scan_type, + "product_name": product_name, + "file_name": report.name, + }).encode(), + headers={ + "Authorization": f"Token {api_token}", + "Content-Type": "application/json", + }, + method="POST", + ) + urllib.request.urlopen(req) + PY + volumeMounts: + - name: workspace + mountPath: /workspace +{{- end }} diff --git a/helm/templates/upload-storage.yaml b/helm/templates/upload-storage.yaml new file mode 100644 index 0000000..7e271fe --- /dev/null +++ b/helm/templates/upload-storage.yaml @@ -0,0 +1,45 @@ +{{- if .Values.pipeline.enabled }} +apiVersion: argoproj.io/v1alpha1 +kind: ClusterWorkflowTemplate +metadata: + name: amp-security-pipeline-v1.0.0 +spec: + templates: + - name: upload-storage + container: + image: amazon/aws-cli:2.15.40 + env: + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: AWS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: AWS_SECRET_ACCESS_KEY + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: MINIO_ROOT_USER + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: amp-security-pipeline-secrets + key: MINIO_ROOT_PASSWORD + command: + - sh + - -c + args: + - | + set -eu + repo_name="${REPO_NAME:-repo}" + commit_sha="${GIT_COMMIT_SHA:-unknown}" + report_date="$(date -u +%F)" + aws s3 sync /workspace/reports "s3://${REPORTS_BUCKET:-security-reports}/${repo_name}/${report_date}/${commit_sha}/" + volumeMounts: + - name: workspace + mountPath: /workspace +{{- end }}