Compare commits

..

6 Commits

19 changed files with 508 additions and 315 deletions
+25
View File
@@ -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
+72 -37
View File
@@ -1,68 +1,103 @@
# agentguard-ci # 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: - Runs TruffleHog for secret scanning.
* **TruffleHog**: Verifies leaked API keys dynamically to prevent false-positives from AI hallucinations. - Runs Semgrep for first-party code scanning.
* **Semgrep**: Scans first-party application logic for vulnerabilities (e.g., SQLi, XSS). - Runs KICS for infrastructure misconfiguration scanning.
* **Socket.dev**: Analyzes dependencies for supply chain attacks, malware, and typosquatting. - Runs Socket.dev for dependency risk scanning.
* **Pulumi CrossGuard**: Validates Infrastructure as Code against policy packs. - Runs Syft and Grype for SBOM generation and vulnerability scanning.
* **Syft + Grype**: Generates SBOMs and scans for container vulnerabilities scored via EPSS. - Runs Pulumi CrossGuard for policy-pack validation.
* **KICS**: Scans infrastructure misconfigurations. - Uploads supported reports to DefectDojo when enabled.
* **DefectDojo & MinIO**: Uploads findings to a centralized ASPM dashboard and raw SARIF/JSON reports to S3-compatible storage. - Uploads raw reports to S3-compatible storage when enabled.
* **Policy Enforcement**: Custom TypeScript logic automatically fails the build if any findings exceed your defined CVSS severity threshold. - 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
* **Argo Workflows** - Infisical Kubernetes Operator, if you want this chart to sync secrets automatically
* **Infisical Kubernetes Operator** (for secret injection) - DefectDojo, if you want report ingestion enabled
* **DefectDojo** (for vulnerability dashboards) - MinIO or another S3-compatible store, if you want raw report uploads enabled
* **MinIO / S3** (for raw report storage)
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 ```bash
cd tools cd tools
docker build -t your-registry/agentguard-tools:latest . docker build -t your-registry/agentguard-tools:latest .
docker push 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 ### 2. Configure values
Update `helm/values.yaml` (if applicable) and configure your Infisical integration:
Start from [`helm/values.yaml`](helm/values.yaml) and set at least:
```yaml ```yaml
pipeline: pipeline:
enabled: true toolsImage:
repository: your-registry/agentguard-tools
tag: latest
infisical: infisical:
workspaceSlug: "your-workspace-id" enabled: true
projectSlug: "your-project-id" workspaceSlug: your-workspace-id
projectSlug: your-project-id
storage:
enabled: false
defectdojo:
enabled: false
``` ```
### 3. Deploy via Helm 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.
Install the pipeline and its associated resources to your cluster:
If you do not use Infisical, create the `amp-security-pipeline-secrets` secret yourself before running the workflow.
### 3. Deploy the chart
```bash ```bash
helm upgrade --install agentguard-ci ./helm -n argo 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.
+6
View File
@@ -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"
+16 -15
View File
@@ -1,17 +1,18 @@
{{- define "template.enforce-policy" }} {{- define "template.enforce-policy" -}}
- name: enforce-policy - name: enforce-policy
inputs: inputs:
parameters: parameters:
- name: fail-on-cvss - name: fail-on-cvss
container: container:
image: agentguard-tools:latest image: "{{ .Values.pipeline.toolsImage.repository }}:{{ .Values.pipeline.toolsImage.tag }}"
command: imagePullPolicy: {{ .Values.pipeline.toolsImage.pullPolicy }}
- node command:
- /app/dist/enforce-policy.js - node
env: - /app/dist/enforce-policy.js
- name: FAIL_ON_CVSS env:
value: "{{inputs.parameters.fail-on-cvss}}" - name: FAIL_ON_CVSS
volumeMounts: value: {{ `{{inputs.parameters.fail-on-cvss}}` | quote }}
- name: workspace volumeMounts:
mountPath: /workspace - name: workspace
mountPath: /workspace
{{- end }} {{- end }}
-33
View File
@@ -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 }}
+23 -20
View File
@@ -1,22 +1,25 @@
{{- define "template.scan-kics" }} {{- define "template.scan-kics" -}}
- name: scan-kics - name: scan-kics
container: inputs:
image: checkmarx/kics:1.7.14 parameters:
command: - name: working-dir
- sh container:
- -c image: {{ .Values.images.kics | quote }}
args: command:
- | - sh
set -eu - -c
mkdir -p /workspace/reports args:
kics scan -p /workspace -o /workspace/reports --report-formats sarif,json --output-name kics || true - |
if [ -f /workspace/reports/kics.sarif ]; then set -eu
exit 0 mkdir -p /workspace/reports
fi kics scan -p "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" -o /workspace/reports --report-formats sarif,json --output-name kics || true
if [ -f /workspace/reports/kics.json ]; then if [ -f /workspace/reports/kics.sarif ]; then
cp /workspace/reports/kics.json /workspace/reports/kics.sarif exit 0
fi fi
volumeMounts: if [ -f /workspace/reports/kics.json ]; then
- name: workspace cp /workspace/reports/kics.json /workspace/reports/kics.sarif
mountPath: /workspace fi
volumeMounts:
- name: workspace
mountPath: /workspace
{{- end }} {{- 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 }}
+17 -14
View File
@@ -1,16 +1,19 @@
{{- define "template.scan-semgrep" }} {{- define "template.scan-semgrep" -}}
- name: scan-semgrep - name: scan-semgrep
container: inputs:
image: returntocorp/semgrep:1.85.0 parameters:
command: - name: working-dir
- sh container:
- -c image: {{ .Values.images.semgrep | quote }}
args: command:
- | - sh
set -eu - -c
mkdir -p /workspace/reports args:
semgrep scan --config auto --sarif --output /workspace/reports/semgrep.sarif /workspace || true - |
volumeMounts: set -eu
- name: workspace mkdir -p /workspace/reports
mountPath: /workspace semgrep scan --config auto --sarif --output /workspace/reports/semgrep.sarif "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" || true
volumeMounts:
- name: workspace
mountPath: /workspace
{{- end }} {{- end }}
+23 -20
View File
@@ -1,22 +1,25 @@
{{- define "template.scan-socketdev" }} {{- define "template.scan-socketdev" -}}
- name: scan-socketdev - name: scan-socketdev
container: inputs:
image: socketdev/socketcli:latest parameters:
env: - name: working-dir
- name: SOCKET_DEV_API_KEY container:
valueFrom: image: {{ .Values.images.socketdev | quote }}
secretKeyRef: env:
name: amp-security-pipeline-secrets - name: SOCKET_DEV_API_KEY
key: SOCKET_DEV_API_KEY valueFrom:
command: secretKeyRef:
- sh name: amp-security-pipeline-secrets
- -c key: SOCKET_DEV_API_KEY
args: command:
- | - sh
set -eu - -c
mkdir -p /workspace/reports args:
socketdev scan /workspace --format json --output /workspace/reports/socketdev.json || true - |
volumeMounts: set -eu
- name: workspace mkdir -p /workspace/reports
mountPath: /workspace socketdev scan "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" --format json --output /workspace/reports/socketdev.json || true
volumeMounts:
- name: workspace
mountPath: /workspace
{{- end }} {{- end }}
+18 -15
View File
@@ -1,17 +1,20 @@
{{- define "template.scan-syft-grype" }} {{- define "template.scan-syft-grype" -}}
- name: scan-syft-grype - name: scan-syft-grype
container: inputs:
image: anchore/syft:latest parameters:
command: - name: working-dir
- sh container:
- -c image: {{ .Values.images.syftGrype | quote }}
args: command:
- | - sh
set -eu - -c
mkdir -p /workspace/reports args:
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 set -eu
volumeMounts: mkdir -p /workspace/reports
- name: workspace syft scan dir:/workspace/{{ `{{inputs.parameters.working-dir}}` }} -o cyclonedx-json=/workspace/reports/sbom.json || true
mountPath: /workspace grype sbom:/workspace/reports/sbom.json -o sarif=/workspace/reports/grype.sarif || true
volumeMounts:
- name: workspace
mountPath: /workspace
{{- end }} {{- end }}
+17 -14
View File
@@ -1,16 +1,19 @@
{{- define "template.scan-trufflehog" }} {{- define "template.scan-trufflehog" -}}
- name: scan-trufflehog - name: scan-trufflehog
container: inputs:
image: trufflesecurity/trufflehog:latest parameters:
command: - name: working-dir
- sh container:
- -c image: {{ .Values.images.trufflehog | quote }}
args: command:
- | - sh
set -eu - -c
mkdir -p /workspace/reports args:
trufflehog filesystem /workspace --json > /workspace/reports/trufflehog.json || true - |
volumeMounts: set -eu
- name: workspace mkdir -p /workspace/reports
mountPath: /workspace trufflehog filesystem "/workspace/{{ `{{inputs.parameters.working-dir}}` }}" --json > /workspace/reports/trufflehog.json || true
volumeMounts:
- name: workspace
mountPath: /workspace
{{- end }} {{- end }}
+37 -20
View File
@@ -1,22 +1,39 @@
{{- define "template.upload-defectdojo" }} {{- define "template.upload-defectdojo" -}}
- name: upload-defectdojo - name: upload-defectdojo
container: container:
image: agentguard-tools:latest image: "{{ .Values.pipeline.toolsImage.repository }}:{{ .Values.pipeline.toolsImage.tag }}"
env: imagePullPolicy: {{ .Values.pipeline.toolsImage.pullPolicy }}
- name: DEFECTDOJO_URL env:
valueFrom: - name: DEFECTDOJO_URL
secretKeyRef: valueFrom:
name: amp-security-pipeline-secrets secretKeyRef:
key: DEFECTDOJO_URL name: amp-security-pipeline-secrets
- name: DEFECTDOJO_API_TOKEN key: DEFECTDOJO_URL
valueFrom: - name: DEFECTDOJO_API_TOKEN
secretKeyRef: valueFrom:
name: amp-security-pipeline-secrets secretKeyRef:
key: DEFECTDOJO_API_TOKEN name: amp-security-pipeline-secrets
command: key: DEFECTDOJO_API_TOKEN
- node - name: DEFECTDOJO_PRODUCT_TYPE_NAME
- /app/dist/upload-defectdojo.js value: {{ .Values.defectdojo.productTypeName | quote }}
volumeMounts: - name: DEFECTDOJO_PRODUCT_NAME
- name: workspace value: {{ .Values.defectdojo.productName | quote }}
mountPath: /workspace - 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 }} {{- end }}
+47 -37
View File
@@ -1,39 +1,49 @@
{{- define "template.upload-storage" }} {{- define "template.upload-storage" -}}
- name: upload-storage - name: upload-storage
container: container:
image: amazon/aws-cli:2.15.40 image: {{ .Values.images.awsCli | quote }}
env: env:
- name: AWS_ACCESS_KEY_ID - name: AWS_ACCESS_KEY_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: amp-security-pipeline-secrets name: amp-security-pipeline-secrets
key: AWS_ACCESS_KEY_ID key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY - name: AWS_SECRET_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: amp-security-pipeline-secrets name: amp-security-pipeline-secrets
key: AWS_SECRET_ACCESS_KEY key: AWS_SECRET_ACCESS_KEY
- name: MINIO_ROOT_USER - name: MINIO_ROOT_USER
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: amp-security-pipeline-secrets name: amp-security-pipeline-secrets
key: MINIO_ROOT_USER key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD - name: MINIO_ROOT_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: amp-security-pipeline-secrets name: amp-security-pipeline-secrets
key: MINIO_ROOT_PASSWORD key: MINIO_ROOT_PASSWORD
command: - name: REPORTS_BUCKET
- sh value: {{ .Values.storage.reportsBucket | quote }}
- -c - name: REPO_NAME
args: value: {{ .Values.pipeline.repoName | quote }}
- | - name: STORAGE_ENDPOINT
set -eu value: {{ .Values.storage.endpoint | quote }}
repo_name="${REPO_NAME:-repo}" command:
commit_sha="${GIT_COMMIT_SHA:-unknown}" - sh
report_date="$(date -u +%F)" - -c
aws s3 sync /workspace/reports "s3://${REPORTS_BUCKET:-security-reports}/${repo_name}/${report_date}/${commit_sha}/" args:
volumeMounts: - |
- name: workspace set -eu
mountPath: /workspace 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 }} {{- end }}
+42 -44
View File
@@ -2,10 +2,11 @@
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate kind: ClusterWorkflowTemplate
metadata: metadata:
name: amp-security-pipeline-v1.0.0 name: {{ .Values.pipeline.name | quote }}
spec: spec:
serviceAccountName: default serviceAccountName: {{ .Values.pipeline.serviceAccountName | quote }}
entrypoint: security-pipeline entrypoint: security-pipeline
onExit: pipeline-exit-hook
volumeClaimTemplates: volumeClaimTemplates:
- metadata: - metadata:
name: workspace name: workspace
@@ -14,16 +15,16 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: 1Gi storage: {{ .Values.pipeline.workspace.storage | quote }}
arguments: arguments:
parameters: parameters:
- name: working-dir - name: working-dir
value: . value: {{ .Values.pipeline.workingDir | quote }}
- name: fail-on-cvss - name: fail-on-cvss
value: "7.0" value: {{ .Values.pipeline.failOnCvss | quote }}
- name: repo-url - name: repo-url
- name: git-revision - name: git-revision
value: main value: {{ .Values.pipeline.gitRevision | quote }}
templates: templates:
- name: security-pipeline - name: security-pipeline
dag: dag:
@@ -33,9 +34,9 @@ spec:
arguments: arguments:
parameters: parameters:
- name: repo-url - name: repo-url
value: "{{workflow.parameters.repo-url}}" value: {{ `{{workflow.parameters.repo-url}}` | quote }}
- name: git-revision - name: git-revision
value: "{{workflow.parameters.git-revision}}" value: {{ `{{workflow.parameters.git-revision}}` | quote }}
- name: scanners - name: scanners
dependencies: dependencies:
- clone - clone
@@ -43,42 +44,39 @@ spec:
arguments: arguments:
parameters: parameters:
- name: working-dir - name: working-dir
value: "{{workflow.parameters.working-dir}}" value: {{ `{{workflow.parameters.working-dir}}` | quote }}
- 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
- name: enforce-policy - name: enforce-policy
dependencies: dependencies:
- upload-storage - scanners
- upload-defectdojo
template: enforce-policy template: enforce-policy
arguments: arguments:
parameters: parameters:
- name: fail-on-cvss - name: fail-on-cvss
value: "{{workflow.parameters.fail-on-cvss}}" value: {{ `{{workflow.parameters.fail-on-cvss}}` | quote }}
- name: sinks-and-enforcement {{- if .Values.storage.enabled }}
- name: upload-storage
dependencies: dependencies:
- scanners - 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 - name: clone-repo
inputs: inputs:
parameters: parameters:
- name: repo-url - name: repo-url
- name: git-revision - name: git-revision
container: container:
image: alpine/git:2.45.2 image: {{ .Values.images.git | quote }}
command: command:
- sh - sh
- -c - -c
args: 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: volumeMounts:
- name: workspace - name: workspace
mountPath: /workspace mountPath: /workspace
@@ -86,37 +84,37 @@ spec:
inputs: inputs:
parameters: parameters:
- name: working-dir - name: working-dir
- name: fail-on-cvss
dag: dag:
tasks: tasks:
{{- range $scanner := list "trufflehog" "semgrep" "kics" "socketdev" "syft-grype" "defectdojo" }} {{- range $scanner := list "trufflehog" "semgrep" "kics" "socketdev" "syft-grype" "pulumi-crossguard" }}
- name: {{ $scanner }} - name: {{ $scanner }}
template: scan-{{ $scanner }} template: scan-{{ $scanner }}
arguments: arguments:
parameters: parameters:
- name: working-dir - name: working-dir
value: "{{inputs.parameters.working-dir}}" value: {{ `{{inputs.parameters.working-dir}}` | quote }}
{{- end }} {{- end }}
- name: sinks-and-enforcement - name: pipeline-exit-hook
container: container:
image: curlimages/curl:latest image: {{ .Values.images.curl | quote }}
command: command:
- sh - sh
- -c - -c
args: args:
- | - |
set -eu set -eu
echo "Pipeline complete. You can configure a webhook notification here." echo "Pipeline completed with status: {{ `{{workflow.status}}` }}"
if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then {{ include "template.scan-trufflehog" . | nindent 4 }}
curl -X POST -H 'Content-type: application/json' --data '{"text":"Security Pipeline Finished"}' "${SLACK_WEBHOOK_URL}" || true {{ include "template.scan-semgrep" . | nindent 4 }}
fi {{ include "template.scan-kics" . | nindent 4 }}
{{ include "template.scan-syft-grype" . | indent 4 }} {{ include "template.scan-socketdev" . | nindent 4 }}
{{ include "template.scan-socketdev" . | indent 4 }} {{ include "template.scan-syft-grype" . | nindent 4 }}
{{ include "template.scan-defectdojo" . | indent 4 }} {{ include "template.scan-pulumi-crossguard" . | nindent 4 }}
{{ include "template.scan-semgrep" . | indent 4 }} {{- if .Values.storage.enabled }}
{{ include "template.scan-trufflehog" . | indent 4 }} {{ include "template.upload-storage" . | nindent 4 }}
{{ include "template.scan-kics" . | indent 4 }} {{- end }}
{{ include "template.upload-defectdojo" . | indent 4 }} {{- if .Values.defectdojo.enabled }}
{{ include "template.upload-storage" . | indent 4 }} {{ include "template.upload-defectdojo" . | nindent 4 }}
{{ include "template.enforce-policy" . | indent 4 }} {{- end }}
{{ include "template.enforce-policy" . | nindent 4 }}
{{- end }} {{- end }}
+1 -1
View File
@@ -1,4 +1,4 @@
{{- if .Values.pipeline.enabled }} {{- if and .Values.pipeline.enabled .Values.infisical.enabled }}
apiVersion: infisical.com/v1alpha1 apiVersion: infisical.com/v1alpha1
kind: InfisicalSecret kind: InfisicalSecret
metadata: metadata:
+49
View File
@@ -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
View File
@@ -1,17 +1,15 @@
{ {
"name": "tools", "name": "tools",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "Custom pipeline utilities for agentguard-ci",
"main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run src",
"build": "tsc" "build": "tsc"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "commonjs",
"devDependencies": { "devDependencies": {
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"tsx": "^4.21.0", "tsx": "^4.21.0",
+73 -39
View File
@@ -1,68 +1,102 @@
import * as fs from 'node:fs'; import { promises as fs } from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
export async function uploadReports() { function resolveScanType(fileName: string): string | undefined {
const baseUrl = (process.env.DEFECTDOJO_URL || "").replace(/\/$/, ""); 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 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) { 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); process.exit(1);
} }
const scanMap: Record<string, string> = { const reportsDir = '/workspace/reports';
".sarif": "SARIF", let fileNames: string[];
".json": "Generic Findings Import",
};
const reportsDir = "/workspace/reports"; try {
if (!fs.existsSync(reportsDir)) { fileNames = (await fs.readdir(reportsDir)).sort();
console.log("No reports directory found."); } catch {
console.log('No reports directory found.');
return; return;
} }
const files = fs.readdirSync(reportsDir).sort(); for (const fileName of fileNames) {
const fullPath = path.join(reportsDir, fileName);
for (const file of files) { const stats = await fs.stat(fullPath);
const fullPath = path.join(reportsDir, file); if (!stats.isFile()) {
if (!fs.statSync(fullPath).isFile()) continue; continue;
}
const ext = path.extname(file);
const scanType = scanMap[ext]; const scanType = resolveScanType(fileName);
if (!scanType) continue; if (!scanType) {
console.log(`Skipping ${fileName}: no DefectDojo importer is configured for this file yet.`);
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 ${fileName} to DefectDojo as ${scanType}...`);
console.log(`Uploading ${file} as ${scanType}...`);
try { try {
const response = await fetch(`${baseUrl}/api/v2/import-scan/`, { const response = await fetch(`${baseUrl}/api/v2/reimport-scan/`, {
method: "POST", method: 'POST',
headers: { headers: {
"Authorization": `Token ${apiToken}`, Authorization: `Token ${apiToken}`,
"Content-Type": "application/json",
}, },
body: JSON.stringify({ body: form,
scan_type: scanType,
product_name: productName,
file_name: file,
})
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text(); 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; process.exitCode = 1;
} else { continue;
console.log(`Successfully uploaded ${file}`);
} }
} 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; process.exitCode = 1;
} }
} }
} }
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[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
View File
@@ -8,7 +8,9 @@
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true,
"types": ["node", "vitest/globals"],
"lib": ["ES2022", "DOM"]
}, },
"include": ["src/**/*"] "include": ["src/**/*.ts"]
} }