other changes
This commit is contained in:
@@ -1,3 +1,68 @@
|
|||||||
# agentguard-ci
|
# agentguard-ci
|
||||||
|
|
||||||
A DevSecOps Argo Workflows pipeline to protect against AI coding agent hallucinations and supply chain attacks.
|
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.
|
||||||
|
|
||||||
|
## 📖 The 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 🏗️ Architecture & Features
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
For deep-dive architecture decisions, see the [Pipeline Overview ADR](docs/pipeline-overview.md) and [Secret Strategy ADR](docs/secret-strategy.md).
|
||||||
|
|
||||||
|
## 🚀 Prerequisites
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
You will also need API keys or tokens for: Socket.dev, Pulumi, AWS/MinIO, and DefectDojo.
|
||||||
|
|
||||||
|
## 🛠️ Installation
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
```yaml
|
||||||
|
pipeline:
|
||||||
|
enabled: true
|
||||||
|
infisical:
|
||||||
|
workspaceSlug: "your-workspace-id"
|
||||||
|
projectSlug: "your-project-id"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy via Helm
|
||||||
|
Install the pipeline and its associated resources to your cluster:
|
||||||
|
```bash
|
||||||
|
helm upgrade --install agentguard-ci ./helm -n argo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Secret Management Integration
|
||||||
|
|
||||||
|
To prevent hardcoded secrets in the pipeline, this project uses the **Infisical Kubernetes Operator**.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
The Argo Workflow then mounts this standard secret as environment variables inside the scanning containers, ensuring zero secret leakage in the Git repository.
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import glob, re, os
|
||||||
|
|
||||||
|
files = glob.glob("helm/templates/scan-*.yaml") + glob.glob("helm/templates/upload-*.yaml") + ["helm/templates/enforce-policy.yaml"]
|
||||||
|
for f in files:
|
||||||
|
with open(f) as file:
|
||||||
|
content = file.read()
|
||||||
|
match = re.search(r'spec:\n templates:\n(.*)(?:{{- end }})', content, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
template_content = match.group(1).strip()
|
||||||
|
# Extract the base name e.g. scan-kics
|
||||||
|
base_name = os.path.basename(f).replace('.yaml', '')
|
||||||
|
new_content = f'{{{{- define "template.{base_name}" }}}}\n{template_content}\n{{{{- end }}}}\n'
|
||||||
|
new_filename = os.path.join(os.path.dirname(f), f"_{base_name}.yaml")
|
||||||
|
with open(new_filename, "w") as out:
|
||||||
|
out.write(new_content)
|
||||||
|
os.remove(f)
|
||||||
@@ -31,7 +31,7 @@ For solo personal projects, a complex CI/CD security pipeline is usually overkil
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### The Chosen Solution: Dual-Layer Approach
|
### The Chosen Solution: Dual-Layer Approach + Infisical Runtime Injection
|
||||||
|
|
||||||
#### Layer 1: Gitleaks (The Local Guard)
|
#### Layer 1: Gitleaks (The Local Guard)
|
||||||
* **Where:** Local developer machine (Pre-commit Hook).
|
* **Where:** Local developer machine (Pre-commit Hook).
|
||||||
@@ -41,6 +41,10 @@ For solo personal projects, a complex CI/CD security pipeline is usually overkil
|
|||||||
* **Where:** GitHub Actions / CI Pipeline (Post-commit).
|
* **Where:** GitHub Actions / CI Pipeline (Post-commit).
|
||||||
* **Why:** Uses active verification. If a secret slips past (via an AI agent pushing directly or a bypassed local hook), TruffleHog actively calls out to external APIs to verify if the key is live. By using the `--only-verified` flag, it guarantees zero false positives and only fails the pipeline if it proves a key is an active threat.
|
* **Why:** Uses active verification. If a secret slips past (via an AI agent pushing directly or a bypassed local hook), TruffleHog actively calls out to external APIs to verify if the key is live. By using the `--only-verified` flag, it guarantees zero false positives and only fails the pipeline if it proves a key is an active threat.
|
||||||
|
|
||||||
|
#### Layer 3: Infisical Operator (Pipeline Runtime Injection)
|
||||||
|
* **Where:** Inside the Kubernetes Cluster (via `InfisicalSecret` CRD).
|
||||||
|
* **Why:** The security pipeline itself requires numerous highly-privileged secrets (DefectDojo API tokens, AWS S3 keys, Pulumi access tokens, Socket.dev keys) to execute the scans and upload reports. We do not store these in GitOps. Instead, the Helm chart deploys an `InfisicalSecret` resource. The Infisical Kubernetes Operator authenticates with the central vault, pulls the secrets dynamically, and syncs them into a native Kubernetes `Secret` (`amp-security-pipeline-secrets`). The Argo Workflow containers then consume these safely at runtime as environment variables.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Tradeoffs & Accepted Risks
|
### Tradeoffs & Accepted Risks
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ metadata:
|
|||||||
data:
|
data:
|
||||||
renovate.json: |
|
renovate.json: |
|
||||||
{
|
{
|
||||||
"extends": ["github>my-org/my-repo//renovate-preset"],
|
"extends": [{{ .Values.preset | quote }}],
|
||||||
"onboarding": false,
|
"onboarding": false,
|
||||||
"platform": "github",
|
"platform": "github",
|
||||||
"repositories": {{ toJson .Values.repositories }}
|
"repositories": {{ toJson .Values.repositories }}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ image:
|
|||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
schedule: "0 * * * *"
|
schedule: "0 * * * *"
|
||||||
|
preset: "github>my-org/my-repo//renovate-preset"
|
||||||
repositories: []
|
repositories: []
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if .Values.pipeline.enabled }}
|
||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: ClusterWorkflowTemplate
|
kind: ClusterWorkflowTemplate
|
||||||
metadata:
|
metadata:
|
||||||
@@ -47,21 +48,11 @@ spec:
|
|||||||
value: "{{workflow.parameters.fail-on-cvss}}"
|
value: "{{workflow.parameters.fail-on-cvss}}"
|
||||||
- name: upload-storage
|
- name: upload-storage
|
||||||
dependencies:
|
dependencies:
|
||||||
- scan-trufflehog
|
- scanners
|
||||||
- scan-semgrep
|
|
||||||
- scan-kics
|
|
||||||
- scan-socketdev
|
|
||||||
- scan-syft-grype
|
|
||||||
- scan-crossguard
|
|
||||||
template: upload-storage
|
template: upload-storage
|
||||||
- name: upload-defectdojo
|
- name: upload-defectdojo
|
||||||
dependencies:
|
dependencies:
|
||||||
- scan-trufflehog
|
- scanners
|
||||||
- scan-semgrep
|
|
||||||
- scan-kics
|
|
||||||
- scan-socketdev
|
|
||||||
- scan-syft-grype
|
|
||||||
- scan-crossguard
|
|
||||||
template: upload-defectdojo
|
template: upload-defectdojo
|
||||||
- name: enforce-policy
|
- name: enforce-policy
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -76,54 +67,6 @@ spec:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- scanners
|
- scanners
|
||||||
template: sinks-and-enforcement
|
template: sinks-and-enforcement
|
||||||
- name: scan-trufflehog
|
|
||||||
dependencies:
|
|
||||||
- clone
|
|
||||||
template: scan-trufflehog
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: working-dir
|
|
||||||
value: "{{workflow.parameters.working-dir}}"
|
|
||||||
- name: scan-semgrep
|
|
||||||
dependencies:
|
|
||||||
- clone
|
|
||||||
template: scan-semgrep
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: working-dir
|
|
||||||
value: "{{workflow.parameters.working-dir}}"
|
|
||||||
- name: scan-kics
|
|
||||||
dependencies:
|
|
||||||
- clone
|
|
||||||
template: scan-kics
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: working-dir
|
|
||||||
value: "{{workflow.parameters.working-dir}}"
|
|
||||||
- name: scan-socketdev
|
|
||||||
dependencies:
|
|
||||||
- clone
|
|
||||||
template: scan-socketdev
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: working-dir
|
|
||||||
value: "{{workflow.parameters.working-dir}}"
|
|
||||||
- name: scan-syft-grype
|
|
||||||
dependencies:
|
|
||||||
- clone
|
|
||||||
template: scan-syft-grype
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: working-dir
|
|
||||||
value: "{{workflow.parameters.working-dir}}"
|
|
||||||
- name: scan-crossguard
|
|
||||||
dependencies:
|
|
||||||
- clone
|
|
||||||
template: scan-crossguard
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: working-dir
|
|
||||||
value: "{{workflow.parameters.working-dir}}"
|
|
||||||
- name: clone-repo
|
- name: clone-repo
|
||||||
inputs:
|
inputs:
|
||||||
parameters:
|
parameters:
|
||||||
@@ -148,39 +91,60 @@ spec:
|
|||||||
tasks:
|
tasks:
|
||||||
- name: trufflehog
|
- name: trufflehog
|
||||||
template: scan-trufflehog
|
template: scan-trufflehog
|
||||||
|
arguments:
|
||||||
|
parameters:
|
||||||
|
- name: working-dir
|
||||||
|
value: "{{inputs.parameters.working-dir}}"
|
||||||
- name: semgrep
|
- name: semgrep
|
||||||
template: scan-semgrep
|
template: scan-semgrep
|
||||||
|
arguments:
|
||||||
|
parameters:
|
||||||
|
- name: working-dir
|
||||||
|
value: "{{inputs.parameters.working-dir}}"
|
||||||
- name: kics
|
- name: kics
|
||||||
template: scan-kics
|
template: scan-kics
|
||||||
|
arguments:
|
||||||
|
parameters:
|
||||||
|
- name: working-dir
|
||||||
|
value: "{{inputs.parameters.working-dir}}"
|
||||||
- name: socketdev
|
- name: socketdev
|
||||||
template: scan-socketdev
|
template: scan-socketdev
|
||||||
|
arguments:
|
||||||
|
parameters:
|
||||||
|
- name: working-dir
|
||||||
|
value: "{{inputs.parameters.working-dir}}"
|
||||||
- name: syft-grype
|
- name: syft-grype
|
||||||
template: scan-syft-grype
|
template: scan-syft-grype
|
||||||
|
arguments:
|
||||||
|
parameters:
|
||||||
|
- name: working-dir
|
||||||
|
value: "{{inputs.parameters.working-dir}}"
|
||||||
- name: defectdojo
|
- name: defectdojo
|
||||||
template: scan-crossguard
|
template: scan-crossguard
|
||||||
|
arguments:
|
||||||
|
parameters:
|
||||||
|
- name: working-dir
|
||||||
|
value: "{{inputs.parameters.working-dir}}"
|
||||||
- name: sinks-and-enforcement
|
- name: sinks-and-enforcement
|
||||||
container:
|
container:
|
||||||
image: alpine:3.20
|
image: curlimages/curl:latest
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
args:
|
args:
|
||||||
- echo "stub: sinks and enforcement"
|
- |
|
||||||
- name: scan-trufflehog
|
set -eu
|
||||||
template: scan-trufflehog
|
echo "Pipeline complete. You can configure a webhook notification here."
|
||||||
- name: scan-semgrep
|
if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
|
||||||
template: scan-semgrep
|
curl -X POST -H 'Content-type: application/json' --data '{"text":"Security Pipeline Finished"}' "${SLACK_WEBHOOK_URL}" || true
|
||||||
- name: scan-kics
|
fi
|
||||||
template: scan-kics
|
{{ include "template.scan-syft-grype" . | indent 4 }}
|
||||||
- name: scan-socketdev
|
{{ include "template.scan-socketdev" . | indent 4 }}
|
||||||
template: scan-socketdev
|
{{ include "template.scan-crossguard" . | indent 4 }}
|
||||||
- name: scan-syft-grype
|
{{ include "template.scan-semgrep" . | indent 4 }}
|
||||||
template: scan-syft-grype
|
{{ include "template.scan-trufflehog" . | indent 4 }}
|
||||||
- name: scan-crossguard
|
{{ include "template.scan-kics" . | indent 4 }}
|
||||||
template: scan-crossguard
|
{{ include "template.upload-defectdojo" . | indent 4 }}
|
||||||
- name: upload-storage
|
{{ include "template.upload-storage" . | indent 4 }}
|
||||||
template: upload-storage
|
{{ include "template.enforce-policy" . | indent 4 }}
|
||||||
- name: upload-defectdojo
|
{{- end }}
|
||||||
template: upload-defectdojo
|
|
||||||
- name: enforce-policy
|
|
||||||
template: enforce-policy
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
{{- 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 }}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# The default command isn't strictly necessary as Argo will override it
|
||||||
|
CMD ["node", "/app/dist/enforce-policy.js"]
|
||||||
Generated
+1853
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"vitest": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user