Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0099dc1e4a | |||
| 193230edac | |||
| 4cf7bf2d57 | |||
| fefe72d177 | |||
| 6f0252776f | |||
| 5bdf3fe114 |
+25
@@ -0,0 +1,25 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Reports and temporary files
|
||||
reports/
|
||||
*.sarif
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -1,68 +1,103 @@
|
||||
# agentguard-ci
|
||||
|
||||
A DevSecOps Argo Workflows pipeline specifically designed to protect against AI coding agent hallucinations, supply chain attacks, and security misconfigurations in a homelab or solo-developer environment.
|
||||
A DevSecOps Argo Workflows pipeline designed to protect against AI coding agent hallucinations, supply chain attacks, and security misconfigurations in a homelab or solo-developer environment.
|
||||
|
||||
## 📖 The Problem
|
||||
## Problem
|
||||
|
||||
AI coding agents are highly productive "junior developers," but they lack intrinsic context. They frequently hallucinate dummy credentials, introduce insecure application logic, or pull in new, potentially typosquatted dependencies.
|
||||
AI coding agents are highly productive junior developers, but they lack intrinsic context. They can hallucinate credentials, introduce insecure logic, or pull in risky dependencies.
|
||||
|
||||
This pipeline acts as a strict, automated gatekeeper that prioritizes zero-noise alerting, allowing you to maintain high development velocity without compromising the security of your exposed homelab.
|
||||
This project adds a reusable security gate in front of deployment by cloning a repository into an Argo workflow, running multiple scanners in parallel, uploading supported results to DefectDojo and object storage, and enforcing a CVSS-based policy threshold.
|
||||
|
||||
## 🏗️ Architecture & Features
|
||||
## What the pipeline does
|
||||
|
||||
This project deploys an **Argo ClusterWorkflowTemplate** that orchestrates a parallel security scanning matrix whenever code is pushed:
|
||||
* **TruffleHog**: Verifies leaked API keys dynamically to prevent false-positives from AI hallucinations.
|
||||
* **Semgrep**: Scans first-party application logic for vulnerabilities (e.g., SQLi, XSS).
|
||||
* **Socket.dev**: Analyzes dependencies for supply chain attacks, malware, and typosquatting.
|
||||
* **Pulumi CrossGuard**: Validates Infrastructure as Code against policy packs.
|
||||
* **Syft + Grype**: Generates SBOMs and scans for container vulnerabilities scored via EPSS.
|
||||
* **KICS**: Scans infrastructure misconfigurations.
|
||||
* **DefectDojo & MinIO**: Uploads findings to a centralized ASPM dashboard and raw SARIF/JSON reports to S3-compatible storage.
|
||||
* **Policy Enforcement**: Custom TypeScript logic automatically fails the build if any findings exceed your defined CVSS severity threshold.
|
||||
- Runs TruffleHog for secret scanning.
|
||||
- Runs Semgrep for first-party code scanning.
|
||||
- Runs KICS for infrastructure misconfiguration scanning.
|
||||
- Runs Socket.dev for dependency risk scanning.
|
||||
- Runs Syft and Grype for SBOM generation and vulnerability scanning.
|
||||
- Runs Pulumi CrossGuard for policy-pack validation.
|
||||
- Uploads supported reports to DefectDojo when enabled.
|
||||
- Uploads raw reports to S3-compatible storage when enabled.
|
||||
- Fails the workflow when findings meet or exceed the configured CVSS threshold.
|
||||
|
||||
For deep-dive architecture decisions, see the [Pipeline Overview ADR](docs/pipeline-overview.md) and [Secret Strategy ADR](docs/secret-strategy.md).
|
||||
## Prerequisites
|
||||
|
||||
## 🚀 Prerequisites
|
||||
Install these separately in your cluster before using this chart:
|
||||
|
||||
Before installing the pipeline, ensure your Kubernetes cluster has the following installed:
|
||||
* **Argo Workflows**
|
||||
* **Infisical Kubernetes Operator** (for secret injection)
|
||||
* **DefectDojo** (for vulnerability dashboards)
|
||||
* **MinIO / S3** (for raw report storage)
|
||||
- Argo Workflows
|
||||
- Infisical Kubernetes Operator, if you want this chart to sync secrets automatically
|
||||
- DefectDojo, if you want report ingestion enabled
|
||||
- MinIO or another S3-compatible store, if you want raw report uploads enabled
|
||||
|
||||
You will also need API keys or tokens for: Socket.dev, Pulumi, AWS/MinIO, and DefectDojo.
|
||||
You will also need the corresponding credentials for Socket.dev, Pulumi, AWS or MinIO, and DefectDojo.
|
||||
|
||||
## 🛠️ Installation
|
||||
## Validation workflow
|
||||
|
||||
For fast validation while wiring up infrastructure, use these tools together:
|
||||
|
||||
- `helm lint ./helm`
|
||||
- `helm template agentguard-ci ./helm`
|
||||
- `helm template agentguard-ci ./helm | kubectl apply --dry-run=client -f -`
|
||||
- `helm template agentguard-ci ./helm | kubectl apply --dry-run=server -f -`
|
||||
- `argo lint rendered.yaml`
|
||||
|
||||
Notes:
|
||||
|
||||
- `helm lint` catches Helm chart problems.
|
||||
- `kubectl --dry-run=client` catches basic Kubernetes schema issues without talking to the cluster.
|
||||
- `kubectl --dry-run=server` is better once the cluster already has the Argo and Infisical CRDs installed.
|
||||
- `argo lint` is the most useful Argo-specific check once you have the Argo CLI installed.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Build the tools image
|
||||
|
||||
The workflow uses custom TypeScript utilities for policy enforcement and DefectDojo uploads.
|
||||
|
||||
### 1. Build the Pipeline Tools Image
|
||||
The pipeline relies on custom TypeScript logic (e.g., CVSS enforcement and API uploads). Build and push this image to your registry:
|
||||
```bash
|
||||
cd tools
|
||||
docker build -t your-registry/agentguard-tools:latest .
|
||||
docker push your-registry/agentguard-tools:latest
|
||||
```
|
||||
*(Make sure to update `clusterworkflowtemplate.yaml` with your custom image if you do not use `agentguard-tools:latest`)*
|
||||
|
||||
### 2. Configure Helm Values
|
||||
Update `helm/values.yaml` (if applicable) and configure your Infisical integration:
|
||||
### 2. Configure values
|
||||
|
||||
Start from [`helm/values.yaml`](helm/values.yaml) and set at least:
|
||||
|
||||
```yaml
|
||||
pipeline:
|
||||
enabled: true
|
||||
toolsImage:
|
||||
repository: your-registry/agentguard-tools
|
||||
tag: latest
|
||||
|
||||
infisical:
|
||||
workspaceSlug: "your-workspace-id"
|
||||
projectSlug: "your-project-id"
|
||||
enabled: true
|
||||
workspaceSlug: your-workspace-id
|
||||
projectSlug: your-project-id
|
||||
|
||||
storage:
|
||||
enabled: false
|
||||
|
||||
defectdojo:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
### 3. Deploy via Helm
|
||||
Install the pipeline and its associated resources to your cluster:
|
||||
Keep `storage.enabled` and `defectdojo.enabled` disabled until those services are actually installed and reachable. Keep `infisical.enabled` disabled until the operator is installed and your project identifiers are ready.
|
||||
|
||||
If you do not use Infisical, create the `amp-security-pipeline-secrets` secret yourself before running the workflow.
|
||||
|
||||
### 3. Deploy the chart
|
||||
|
||||
```bash
|
||||
helm upgrade --install agentguard-ci ./helm -n argo
|
||||
```
|
||||
|
||||
## 🔐 Secret Management Integration
|
||||
## DefectDojo integration
|
||||
|
||||
To prevent hardcoded secrets in the pipeline, this project uses the **Infisical Kubernetes Operator**.
|
||||
DefectDojo is not installed by this repository.
|
||||
|
||||
When you deploy the Helm chart, it creates an `InfisicalSecret` Custom Resource (`helm/templates/infisical-secret.yaml`). The Infisical Operator securely fetches your vault secrets (like `SOCKET_DEV_API_KEY` and `DEFECTDOJO_API_TOKEN`) and synchronizes them into a standard Kubernetes `Secret` named `amp-security-pipeline-secrets`.
|
||||
You install DefectDojo separately, then enable this chart's upload step. When enabled, the workflow uploads supported reports into DefectDojo through the API using the custom uploader in [`tools/src/upload-defectdojo.ts`](tools/src/upload-defectdojo.ts).
|
||||
|
||||
The Argo Workflow then mounts this standard secret as environment variables inside the scanning containers, ensuring zero secret leakage in the Git repository.
|
||||
## Secret management
|
||||
|
||||
When `infisical.enabled` is `true`, this chart creates an `InfisicalSecret` that syncs the runtime credentials needed by the workflow into the `amp-security-pipeline-secrets` Kubernetes secret.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: agentguard-ci
|
||||
description: Argo Workflows security pipeline for AI-assisted repositories
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
@@ -1,17 +1,18 @@
|
||||
{{- define "template.enforce-policy" }}
|
||||
{{- define "template.enforce-policy" -}}
|
||||
- name: enforce-policy
|
||||
inputs:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
container:
|
||||
image: agentguard-tools:latest
|
||||
command:
|
||||
- node
|
||||
- /app/dist/enforce-policy.js
|
||||
env:
|
||||
- name: FAIL_ON_CVSS
|
||||
value: "{{inputs.parameters.fail-on-cvss}}"
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
inputs:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
container:
|
||||
image: "{{ .Values.pipeline.toolsImage.repository }}:{{ .Values.pipeline.toolsImage.tag }}"
|
||||
imagePullPolicy: {{ .Values.pipeline.toolsImage.pullPolicy }}
|
||||
command:
|
||||
- node
|
||||
- /app/dist/enforce-policy.js
|
||||
env:
|
||||
- name: FAIL_ON_CVSS
|
||||
value: {{ `{{inputs.parameters.fail-on-cvss}}` | quote }}
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{{- define "template.scan-defectdojo" }}
|
||||
- name: scan-defectdojo
|
||||
container:
|
||||
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:
|
||||
- |
|
||||
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
|
||||
{{- end }}
|
||||
@@ -1,22 +1,25 @@
|
||||
{{- define "template.scan-kics" }}
|
||||
{{- define "template.scan-kics" -}}
|
||||
- name: scan-kics
|
||||
container:
|
||||
image: checkmarx/kics:1.7.14
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
kics scan -p /workspace -o /workspace/reports --report-formats sarif,json --output-name kics || true
|
||||
if [ -f /workspace/reports/kics.sarif ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ -f /workspace/reports/kics.json ]; then
|
||||
cp /workspace/reports/kics.json /workspace/reports/kics.sarif
|
||||
fi
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.kics | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
kics scan -p "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" -o /workspace/reports --report-formats sarif,json --output-name kics || true
|
||||
if [ -f /workspace/reports/kics.sarif ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ -f /workspace/reports/kics.json ]; then
|
||||
cp /workspace/reports/kics.json /workspace/reports/kics.sarif
|
||||
fi
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{{- define "template.scan-pulumi-crossguard" -}}
|
||||
- name: scan-pulumi-crossguard
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.pulumiCrossguard | quote }}
|
||||
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:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
cd "/workspace/{{ `{{inputs.parameters.working-dir}}` }}"
|
||||
pulumi preview --policy-pack "{{ .Values.pulumi.policyPackPath }}" > /workspace/reports/pulumi-crossguard.json 2>&1 || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
@@ -1,16 +1,19 @@
|
||||
{{- define "template.scan-semgrep" }}
|
||||
{{- define "template.scan-semgrep" -}}
|
||||
- name: scan-semgrep
|
||||
container:
|
||||
image: returntocorp/semgrep:1.85.0
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
semgrep scan --config auto --sarif --output /workspace/reports/semgrep.sarif /workspace || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.semgrep | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
semgrep scan --config auto --sarif --output /workspace/reports/semgrep.sarif "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
{{- define "template.scan-socketdev" }}
|
||||
{{- define "template.scan-socketdev" -}}
|
||||
- name: scan-socketdev
|
||||
container:
|
||||
image: socketdev/socketcli:latest
|
||||
env:
|
||||
- name: SOCKET_DEV_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
key: SOCKET_DEV_API_KEY
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
socketdev scan /workspace --format json --output /workspace/reports/socketdev.json || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.socketdev | quote }}
|
||||
env:
|
||||
- name: SOCKET_DEV_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
key: SOCKET_DEV_API_KEY
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
socketdev scan "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" --format json --output /workspace/reports/socketdev.json || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{{- define "template.scan-syft-grype" }}
|
||||
{{- define "template.scan-syft-grype" -}}
|
||||
- name: scan-syft-grype
|
||||
container:
|
||||
image: anchore/syft:latest
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
syft scan dir:/workspace -o cyclonedx-json=/workspace/reports/sbom.json || true
|
||||
grype sbom:/workspace/reports/sbom.json -o sarif=/workspace/reports/grype.sarif || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.syftGrype | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
syft scan dir:/workspace/{{ `{{inputs.parameters.working-dir}}` }} -o cyclonedx-json=/workspace/reports/sbom.json || true
|
||||
grype sbom:/workspace/reports/sbom.json -o sarif=/workspace/reports/grype.sarif || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
{{- define "template.scan-trufflehog" }}
|
||||
{{- define "template.scan-trufflehog" -}}
|
||||
- name: scan-trufflehog
|
||||
container:
|
||||
image: trufflesecurity/trufflehog:latest
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
trufflehog filesystem /workspace --json > /workspace/reports/trufflehog.json || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.trufflehog | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
trufflehog filesystem "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" --json > /workspace/reports/trufflehog.json || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
{{- define "template.upload-defectdojo" }}
|
||||
{{- define "template.upload-defectdojo" -}}
|
||||
- name: upload-defectdojo
|
||||
container:
|
||||
image: agentguard-tools:latest
|
||||
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:
|
||||
- node
|
||||
- /app/dist/upload-defectdojo.js
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
container:
|
||||
image: "{{ .Values.pipeline.toolsImage.repository }}:{{ .Values.pipeline.toolsImage.tag }}"
|
||||
imagePullPolicy: {{ .Values.pipeline.toolsImage.pullPolicy }}
|
||||
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
|
||||
- name: DEFECTDOJO_PRODUCT_TYPE_NAME
|
||||
value: {{ .Values.defectdojo.productTypeName | quote }}
|
||||
- name: DEFECTDOJO_PRODUCT_NAME
|
||||
value: {{ .Values.defectdojo.productName | quote }}
|
||||
- name: DEFECTDOJO_ENGAGEMENT_NAME
|
||||
value: {{ .Values.defectdojo.engagementName | quote }}
|
||||
- name: DEFECTDOJO_MINIMUM_SEVERITY
|
||||
value: {{ .Values.defectdojo.minimumSeverity | quote }}
|
||||
- name: DEFECTDOJO_ACTIVE
|
||||
value: {{ .Values.defectdojo.active | quote }}
|
||||
- name: DEFECTDOJO_VERIFIED
|
||||
value: {{ .Values.defectdojo.verified | quote }}
|
||||
- name: DEFECTDOJO_CLOSE_OLD_FINDINGS
|
||||
value: {{ .Values.defectdojo.closeOldFindings | quote }}
|
||||
- name: DEFECTDOJO_AUTO_CREATE_CONTEXT
|
||||
value: {{ .Values.defectdojo.autoCreateContext | quote }}
|
||||
command:
|
||||
- node
|
||||
- /app/dist/upload-defectdojo.js
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -1,39 +1,49 @@
|
||||
{{- define "template.upload-storage" }}
|
||||
{{- define "template.upload-storage" -}}
|
||||
- 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
|
||||
container:
|
||||
image: {{ .Values.images.awsCli | quote }}
|
||||
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
|
||||
- name: REPORTS_BUCKET
|
||||
value: {{ .Values.storage.reportsBucket | quote }}
|
||||
- name: REPO_NAME
|
||||
value: {{ .Values.pipeline.repoName | quote }}
|
||||
- name: STORAGE_ENDPOINT
|
||||
value: {{ .Values.storage.endpoint | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
commit_sha="${GIT_COMMIT_SHA:-unknown}"
|
||||
report_date="$(date -u +%F)"
|
||||
sync_target="s3://${REPORTS_BUCKET}/${REPO_NAME}/${report_date}/${commit_sha}/"
|
||||
if [ -n "${STORAGE_ENDPOINT}" ]; then
|
||||
aws --endpoint-url "${STORAGE_ENDPOINT}" s3 sync /workspace/reports "${sync_target}"
|
||||
else
|
||||
aws s3 sync /workspace/reports "${sync_target}"
|
||||
fi
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ClusterWorkflowTemplate
|
||||
metadata:
|
||||
name: amp-security-pipeline-v1.0.0
|
||||
name: {{ .Values.pipeline.name | quote }}
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
serviceAccountName: {{ .Values.pipeline.serviceAccountName | quote }}
|
||||
entrypoint: security-pipeline
|
||||
onExit: pipeline-exit-hook
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: workspace
|
||||
@@ -14,16 +15,16 @@ spec:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storage: {{ .Values.pipeline.workspace.storage | quote }}
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: .
|
||||
value: {{ .Values.pipeline.workingDir | quote }}
|
||||
- name: fail-on-cvss
|
||||
value: "7.0"
|
||||
value: {{ .Values.pipeline.failOnCvss | quote }}
|
||||
- name: repo-url
|
||||
- name: git-revision
|
||||
value: main
|
||||
value: {{ .Values.pipeline.gitRevision | quote }}
|
||||
templates:
|
||||
- name: security-pipeline
|
||||
dag:
|
||||
@@ -33,9 +34,9 @@ spec:
|
||||
arguments:
|
||||
parameters:
|
||||
- name: repo-url
|
||||
value: "{{workflow.parameters.repo-url}}"
|
||||
value: {{ `{{workflow.parameters.repo-url}}` | quote }}
|
||||
- name: git-revision
|
||||
value: "{{workflow.parameters.git-revision}}"
|
||||
value: {{ `{{workflow.parameters.git-revision}}` | quote }}
|
||||
- name: scanners
|
||||
dependencies:
|
||||
- clone
|
||||
@@ -43,42 +44,39 @@ spec:
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{workflow.parameters.working-dir}}"
|
||||
- name: fail-on-cvss
|
||||
value: "{{workflow.parameters.fail-on-cvss}}"
|
||||
- name: upload-storage
|
||||
dependencies:
|
||||
- scanners
|
||||
template: upload-storage
|
||||
- name: upload-defectdojo
|
||||
dependencies:
|
||||
- scanners
|
||||
template: upload-defectdojo
|
||||
value: {{ `{{workflow.parameters.working-dir}}` | quote }}
|
||||
- name: enforce-policy
|
||||
dependencies:
|
||||
- upload-storage
|
||||
- upload-defectdojo
|
||||
- scanners
|
||||
template: enforce-policy
|
||||
arguments:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
value: "{{workflow.parameters.fail-on-cvss}}"
|
||||
- name: sinks-and-enforcement
|
||||
value: {{ `{{workflow.parameters.fail-on-cvss}}` | quote }}
|
||||
{{- if .Values.storage.enabled }}
|
||||
- name: upload-storage
|
||||
dependencies:
|
||||
- scanners
|
||||
template: sinks-and-enforcement
|
||||
template: upload-storage
|
||||
{{- end }}
|
||||
{{- if .Values.defectdojo.enabled }}
|
||||
- name: upload-defectdojo
|
||||
dependencies:
|
||||
- scanners
|
||||
template: upload-defectdojo
|
||||
{{- end }}
|
||||
- name: clone-repo
|
||||
inputs:
|
||||
parameters:
|
||||
- name: repo-url
|
||||
- name: git-revision
|
||||
container:
|
||||
image: alpine/git:2.45.2
|
||||
image: {{ .Values.images.git | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- git clone --branch "{{inputs.parameters.git-revision}}" --single-branch "{{inputs.parameters.repo-url}}" /workspace
|
||||
- git clone --branch {{ `{{inputs.parameters.git-revision}}` | quote }} --single-branch {{ `{{inputs.parameters.repo-url}}` | quote }} /workspace
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
@@ -86,37 +84,37 @@ spec:
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
- name: fail-on-cvss
|
||||
dag:
|
||||
tasks:
|
||||
{{- range $scanner := list "trufflehog" "semgrep" "kics" "socketdev" "syft-grype" "defectdojo" }}
|
||||
{{- range $scanner := list "trufflehog" "semgrep" "kics" "socketdev" "syft-grype" "pulumi-crossguard" }}
|
||||
- name: {{ $scanner }}
|
||||
template: scan-{{ $scanner }}
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
value: {{ `{{inputs.parameters.working-dir}}` | quote }}
|
||||
{{- end }}
|
||||
- name: sinks-and-enforcement
|
||||
- name: pipeline-exit-hook
|
||||
container:
|
||||
image: curlimages/curl:latest
|
||||
image: {{ .Values.images.curl | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
echo "Pipeline complete. You can configure a webhook notification here."
|
||||
if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
|
||||
curl -X POST -H 'Content-type: application/json' --data '{"text":"Security Pipeline Finished"}' "${SLACK_WEBHOOK_URL}" || true
|
||||
fi
|
||||
{{ include "template.scan-syft-grype" . | indent 4 }}
|
||||
{{ include "template.scan-socketdev" . | indent 4 }}
|
||||
{{ include "template.scan-defectdojo" . | indent 4 }}
|
||||
{{ include "template.scan-semgrep" . | indent 4 }}
|
||||
{{ include "template.scan-trufflehog" . | indent 4 }}
|
||||
{{ include "template.scan-kics" . | indent 4 }}
|
||||
{{ include "template.upload-defectdojo" . | indent 4 }}
|
||||
{{ include "template.upload-storage" . | indent 4 }}
|
||||
{{ include "template.enforce-policy" . | indent 4 }}
|
||||
echo "Pipeline completed with status: {{ `{{workflow.status}}` }}"
|
||||
{{ include "template.scan-trufflehog" . | nindent 4 }}
|
||||
{{ include "template.scan-semgrep" . | nindent 4 }}
|
||||
{{ include "template.scan-kics" . | nindent 4 }}
|
||||
{{ include "template.scan-socketdev" . | nindent 4 }}
|
||||
{{ include "template.scan-syft-grype" . | nindent 4 }}
|
||||
{{ include "template.scan-pulumi-crossguard" . | nindent 4 }}
|
||||
{{- if .Values.storage.enabled }}
|
||||
{{ include "template.upload-storage" . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.defectdojo.enabled }}
|
||||
{{ include "template.upload-defectdojo" . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{ include "template.enforce-policy" . | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if .Values.pipeline.enabled }}
|
||||
{{- if and .Values.pipeline.enabled .Values.infisical.enabled }}
|
||||
apiVersion: infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
pipeline:
|
||||
enabled: true
|
||||
name: amp-security-pipeline-v1.0.0
|
||||
serviceAccountName: default
|
||||
workingDir: .
|
||||
gitRevision: main
|
||||
failOnCvss: "7.0"
|
||||
workspace:
|
||||
storage: 1Gi
|
||||
repoName: agentguard-ci
|
||||
toolsImage:
|
||||
repository: agentguard-tools
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
images:
|
||||
git: alpine/git:2.45.2
|
||||
trufflehog: trufflesecurity/trufflehog:latest
|
||||
semgrep: returntocorp/semgrep:1.85.0
|
||||
kics: checkmarx/kics:1.7.14
|
||||
socketdev: socketdev/socketcli:latest
|
||||
syftGrype: anchore/syft:latest
|
||||
pulumiCrossguard: pulumi/pulumi:3.154.0
|
||||
awsCli: amazon/aws-cli:2.15.40
|
||||
curl: curlimages/curl:8.8.0
|
||||
|
||||
storage:
|
||||
enabled: false
|
||||
reportsBucket: security-reports
|
||||
endpoint: ""
|
||||
|
||||
pulumi:
|
||||
policyPackPath: policy-pack
|
||||
|
||||
defectdojo:
|
||||
enabled: false
|
||||
productTypeName: Homelab Security
|
||||
productName: agentguard-ci
|
||||
engagementName: Default Pipeline
|
||||
minimumSeverity: Info
|
||||
active: true
|
||||
verified: true
|
||||
closeOldFindings: false
|
||||
autoCreateContext: true
|
||||
|
||||
infisical:
|
||||
enabled: false
|
||||
workspaceSlug: ""
|
||||
projectSlug: ""
|
||||
+2
-4
@@ -1,17 +1,15 @@
|
||||
{
|
||||
"name": "tools",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"description": "Custom pipeline utilities for agentguard-ci",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test": "vitest run src",
|
||||
"build": "tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"tsx": "^4.21.0",
|
||||
|
||||
@@ -1,68 +1,102 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { promises 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(/\/$/, "");
|
||||
function resolveScanType(fileName: string): string | undefined {
|
||||
if (fileName.endsWith('.sarif')) {
|
||||
return 'SARIF';
|
||||
}
|
||||
|
||||
if (fileName === 'generic-findings.json') {
|
||||
return 'Generic Findings Import';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function uploadReports(): Promise<void> {
|
||||
const baseUrl = (process.env.DEFECTDOJO_URL || '').replace(/\/$/, '');
|
||||
const apiToken = process.env.DEFECTDOJO_API_TOKEN;
|
||||
const productName = process.env.DEFECTDOJO_PRODUCT_NAME || "agentguard-ci";
|
||||
const productTypeName = process.env.DEFECTDOJO_PRODUCT_TYPE_NAME || 'Homelab Security';
|
||||
const productName = process.env.DEFECTDOJO_PRODUCT_NAME || 'agentguard-ci';
|
||||
const engagementName = process.env.DEFECTDOJO_ENGAGEMENT_NAME || 'Default Pipeline';
|
||||
const minimumSeverity = process.env.DEFECTDOJO_MINIMUM_SEVERITY || 'Info';
|
||||
const active = process.env.DEFECTDOJO_ACTIVE || 'true';
|
||||
const verified = process.env.DEFECTDOJO_VERIFIED || 'true';
|
||||
const closeOldFindings = process.env.DEFECTDOJO_CLOSE_OLD_FINDINGS || 'false';
|
||||
const autoCreateContext = process.env.DEFECTDOJO_AUTO_CREATE_CONTEXT || 'true';
|
||||
|
||||
if (!baseUrl || !apiToken) {
|
||||
console.error("DEFECTDOJO_URL and DEFECTDOJO_API_TOKEN must be set.");
|
||||
console.error('DEFECTDOJO_URL and DEFECTDOJO_API_TOKEN must be set.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scanMap: Record<string, string> = {
|
||||
".sarif": "SARIF",
|
||||
".json": "Generic Findings Import",
|
||||
};
|
||||
const reportsDir = '/workspace/reports';
|
||||
let fileNames: string[];
|
||||
|
||||
const reportsDir = "/workspace/reports";
|
||||
if (!fs.existsSync(reportsDir)) {
|
||||
console.log("No reports directory found.");
|
||||
try {
|
||||
fileNames = (await fs.readdir(reportsDir)).sort();
|
||||
} catch {
|
||||
console.log('No reports directory found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(reportsDir).sort();
|
||||
for (const fileName of fileNames) {
|
||||
const fullPath = path.join(reportsDir, fileName);
|
||||
const stats = await fs.stat(fullPath);
|
||||
if (!stats.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(reportsDir, file);
|
||||
if (!fs.statSync(fullPath).isFile()) continue;
|
||||
const scanType = resolveScanType(fileName);
|
||||
if (!scanType) {
|
||||
console.log(`Skipping ${fileName}: no DefectDojo importer is configured for this file yet.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(file);
|
||||
const scanType = scanMap[ext];
|
||||
if (!scanType) continue;
|
||||
const reportContents = await fs.readFile(fullPath);
|
||||
const form = new FormData();
|
||||
form.append('scan_type', scanType);
|
||||
form.append('product_type_name', productTypeName);
|
||||
form.append('product_name', productName);
|
||||
form.append('engagement_name', engagementName);
|
||||
form.append('test_title', fileName);
|
||||
form.append('minimum_severity', minimumSeverity);
|
||||
form.append('active', active);
|
||||
form.append('verified', verified);
|
||||
form.append('close_old_findings', closeOldFindings);
|
||||
form.append('auto_create_context', autoCreateContext);
|
||||
form.append('file', new Blob([reportContents]), fileName);
|
||||
|
||||
console.log(`Uploading ${file} as ${scanType}...`);
|
||||
console.log(`Uploading ${fileName} to DefectDojo as ${scanType}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/v2/import-scan/`, {
|
||||
method: "POST",
|
||||
const response = await fetch(`${baseUrl}/api/v2/reimport-scan/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Authorization": `Token ${apiToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Token ${apiToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
scan_type: scanType,
|
||||
product_name: productName,
|
||||
file_name: file,
|
||||
})
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error(`Failed to upload ${file}: ${response.status} ${response.statusText} - ${text}`);
|
||||
console.error(`Failed to upload ${fileName}: ${response.status} ${response.statusText} - ${text}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Successfully uploaded ${file}`);
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Network error uploading ${file}:`, e);
|
||||
|
||||
console.log(`Successfully uploaded ${fileName}`);
|
||||
} catch (error) {
|
||||
console.error(`Network error uploading ${fileName}:`, error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
uploadReports();
|
||||
uploadReports().catch((error: unknown) => {
|
||||
console.error('Unexpected upload failure:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
+4
-2
@@ -8,7 +8,9 @@
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node", "vitest/globals"],
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user