diff --git a/helm/templates/upload-defectdojo.yaml b/helm/templates/upload-defectdojo.yaml deleted file mode 100644 index 9504b9a..0000000 --- a/helm/templates/upload-defectdojo.yaml +++ /dev/null @@ -1,66 +0,0 @@ -{{- 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 deleted file mode 100644 index 7e271fe..0000000 --- a/helm/templates/upload-storage.yaml +++ /dev/null @@ -1,45 +0,0 @@ -{{- 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 }} diff --git a/tools/src/enforce-policy.ts b/tools/src/enforce-policy.ts new file mode 100644 index 0000000..1be9e1f --- /dev/null +++ b/tools/src/enforce-policy.ts @@ -0,0 +1,85 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +export function checkReports(reportsDir: string, threshold: number): { name: string; score: number }[] { + const findings: { name: string; score: number }[] = []; + if (!fs.existsSync(reportsDir)) return findings; + + const files = fs.readdirSync(reportsDir).sort(); + + for (const file of files) { + const fullPath = path.join(reportsDir, file); + if (!fs.statSync(fullPath).isFile()) continue; + + const text = fs.readFileSync(fullPath, 'utf-8'); + let data: any; + try { + data = JSON.parse(text); + } catch (e) { + console.error(`Error parsing ${file}: Invalid JSON`); + process.exitCode = 1; + continue; + } + + if (file.endsWith('.sarif')) { + const runs = data.runs || []; + for (const run of runs) { + const results = run.results || []; + for (const result of results) { + const sev = result.properties?.['security-severity']; + if (sev === undefined) continue; + + const score = parseFloat(sev); + if (isNaN(score)) continue; + + if (score >= threshold) { + findings.push({ name: file, score }); + } + } + } + } else if (file.endsWith('.json')) { + const items = data.findings || data.vulnerabilities || []; + for (const item of items) { + const rawScore = item.cvss || item.score; + if (rawScore === undefined) continue; + + const score = parseFloat(rawScore); + if (isNaN(score)) continue; + + if (score >= threshold) { + findings.push({ name: file, score }); + } + } + } + } + + return findings; +} + +// Ensure the code runs when executed directly +import { fileURLToPath } from 'node:url'; + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const thresholdStr = process.env.FAIL_ON_CVSS; + if (!thresholdStr) { + console.error("FAIL_ON_CVSS environment variable is required."); + process.exit(1); + } + const threshold = parseFloat(thresholdStr); + if (isNaN(threshold)) { + console.error("FAIL_ON_CVSS must be a number."); + process.exit(1); + } + + const reportsDir = "/workspace/reports"; + const findings = checkReports(reportsDir, threshold); + + if (findings.length > 0) { + for (const finding of findings) { + console.error(`${finding.name}: CVSS ${finding.score} >= ${threshold}`); + } + process.exit(1); + } else { + console.log(`No findings met or exceeded CVSS ${threshold}`); + } +} diff --git a/tools/src/upload-defectdojo.ts b/tools/src/upload-defectdojo.ts new file mode 100644 index 0000000..a8839a4 --- /dev/null +++ b/tools/src/upload-defectdojo.ts @@ -0,0 +1,68 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export async function uploadReports() { + const baseUrl = (process.env.DEFECTDOJO_URL || "").replace(/\/$/, ""); + const apiToken = process.env.DEFECTDOJO_API_TOKEN; + const productName = process.env.DEFECTDOJO_PRODUCT_NAME || "agentguard-ci"; + + if (!baseUrl || !apiToken) { + console.error("DEFECTDOJO_URL and DEFECTDOJO_API_TOKEN must be set."); + process.exit(1); + } + + const scanMap: Record = { + ".sarif": "SARIF", + ".json": "Generic Findings Import", + }; + + const reportsDir = "/workspace/reports"; + if (!fs.existsSync(reportsDir)) { + console.log("No reports directory found."); + return; + } + + const files = fs.readdirSync(reportsDir).sort(); + + for (const file of files) { + const fullPath = path.join(reportsDir, file); + if (!fs.statSync(fullPath).isFile()) continue; + + const ext = path.extname(file); + const scanType = scanMap[ext]; + if (!scanType) continue; + + console.log(`Uploading ${file} as ${scanType}...`); + + try { + const response = await fetch(`${baseUrl}/api/v2/import-scan/`, { + method: "POST", + headers: { + "Authorization": `Token ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + scan_type: scanType, + product_name: productName, + file_name: file, + }) + }); + + if (!response.ok) { + const text = await response.text(); + console.error(`Failed to upload ${file}: ${response.status} ${response.statusText} - ${text}`); + process.exitCode = 1; + } else { + console.log(`Successfully uploaded ${file}`); + } + } catch (e) { + console.error(`Network error uploading ${file}:`, e); + process.exitCode = 1; + } + } +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + uploadReports(); +}