Compare commits
32 Commits
2225bb2045
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f2b5c1b47 | |||
| 43d6ad6d6c | |||
| 2d246bd95c | |||
| 0d80215207 | |||
| e0e7018a55 | |||
| 3676ccf990 | |||
| 1e849976aa | |||
| 35ad38dda7 | |||
| 78f30b9608 | |||
| 749afaebf7 | |||
| 0099dc1e4a | |||
| 193230edac | |||
| 4cf7bf2d57 | |||
| fefe72d177 | |||
| 6f0252776f | |||
| 5bdf3fe114 | |||
| 7f366204a9 | |||
| 1036fce55e | |||
| 38ff2f4fde | |||
| f0b937deb7 | |||
| 251070dd77 | |||
| 7587c285e7 | |||
| d8ee53395a | |||
| 5e31efd464 | |||
| ebd29176d0 | |||
| aa907060a4 | |||
| a9224a41c1 | |||
| 35d8630bf2 | |||
| df10609df5 | |||
| 8c2c420bff | |||
| 963e020efa | |||
| 89b3586030 |
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
"${repo_root}/scripts/check-chart.sh"
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
|
||||
.amp-usage.db
|
||||
@@ -1,3 +1,130 @@
|
||||
# agentguard-ci
|
||||
|
||||
A DevSecOps Argo Workflows pipeline to protect against AI coding agent hallucinations and supply chain attacks.
|
||||
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.
|
||||
|
||||
## Problem
|
||||
|
||||
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 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.
|
||||
|
||||
## What the pipeline does
|
||||
|
||||
- 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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install these separately in your cluster before using this chart:
|
||||
|
||||
- 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 the corresponding credentials for Socket.dev, Pulumi, S3-compatible object storage, and DefectDojo.
|
||||
|
||||
## Reading the chart
|
||||
|
||||
If the Helm templates start to feel too abstract, use these two files together:
|
||||
|
||||
- [`helm/values.schema.json`](helm/values.schema.json) documents the expected shape and meaning of the values file.
|
||||
- [`docs/rendered/default-clusterworkflowtemplate.yaml`](docs/rendered/default-clusterworkflowtemplate.yaml) shows the default rendered `ClusterWorkflowTemplate` without Helm directives in the way.
|
||||
|
||||
The rendered reference reflects the default values in `helm/values.yaml`, so optional storage, DefectDojo, and Infisical resources are intentionally omitted there.
|
||||
|
||||
## Validation workflow
|
||||
|
||||
For fast validation while wiring up infrastructure, use these tools together:
|
||||
|
||||
- `./scripts/check-chart.sh`
|
||||
- `RUN_KUBECTL_CLIENT_CHECK=1 ./scripts/check-chart.sh`
|
||||
- `RUN_KUBECTL_SERVER_CHECK=1 ./scripts/check-chart.sh`
|
||||
|
||||
What each mode does:
|
||||
|
||||
- `./scripts/check-chart.sh` runs the fast offline checks used by the repo-managed pre-commit hook: `helm lint`, `helm template`, and `argo lint --offline`.
|
||||
- `RUN_KUBECTL_CLIENT_CHECK=1 ./scripts/check-chart.sh` adds a client-side `kubectl` dry-run. This is optional because CRD-heavy manifests can still be environment-sensitive here.
|
||||
- `RUN_KUBECTL_SERVER_CHECK=1 ./scripts/check-chart.sh` adds a server-side dry-run against your current cluster context, which is the strongest validation once the Argo and Infisical CRDs are installed.
|
||||
|
||||
Install the shared git hook once per clone:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `helm lint` catches Helm chart problems.
|
||||
- `helm template` proves the chart renders successfully with the current values.
|
||||
- `argo lint --offline` is the most useful Argo-specific local check because it validates the rendered `ClusterWorkflowTemplate` without needing cluster access.
|
||||
- `kubectl --dry-run=client` is weaker for CRDs than Argo lint, so it is included as an optional extra check instead of the default hook behavior.
|
||||
- `kubectl --dry-run=server` is best once the cluster already has the Argo and Infisical CRDs installed.
|
||||
- CI should still rerun the same baseline checks even if pre-commit already passed, because hooks are local and bypassable. The usual CI extra is the server-side `kubectl` dry-run once a cluster with the needed CRDs is available.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Build the tools image
|
||||
|
||||
The workflow uses custom TypeScript utilities for policy enforcement and DefectDojo uploads.
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
docker build -t your-registry/agentguard-tools:latest .
|
||||
docker push your-registry/agentguard-tools:latest
|
||||
```
|
||||
|
||||
### 2. Configure values
|
||||
|
||||
Start from [`helm/values.yaml`](helm/values.yaml) and set at least:
|
||||
|
||||
```yaml
|
||||
pipeline:
|
||||
toolsImage:
|
||||
repository: your-registry/agentguard-tools
|
||||
tag: latest
|
||||
|
||||
infisical:
|
||||
enabled: true
|
||||
workspaceSlug: your-workspace-id
|
||||
projectSlug: your-project-id
|
||||
|
||||
storage:
|
||||
enabled: false
|
||||
|
||||
defectdojo:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
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. For storage uploads, the secret should contain `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`.
|
||||
|
||||
### 3. Deploy the chart
|
||||
|
||||
```bash
|
||||
helm upgrade --install agentguard-ci ./helm -n argo
|
||||
```
|
||||
|
||||
## Scope and boundaries
|
||||
|
||||
This repository is intentionally focused on **source, IaC, and dependency scanning** before deployment.
|
||||
|
||||
It does **not** try to be the full build-signing, deploy-admission, or runtime-security stack. For the explicit boundary, missing controls, and recommended sibling pipeline responsibilities, read [`docs/security-scope.md`](docs/security-scope.md).
|
||||
|
||||
## DefectDojo integration
|
||||
|
||||
DefectDojo is not installed by this repository.
|
||||
|
||||
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).
|
||||
|
||||
## 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.
|
||||
|
||||
+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)
|
||||
@@ -0,0 +1,7 @@
|
||||
Nuclei - https://github.com/projectdiscovery/nuclei
|
||||
|
||||
cli tester with embedded chromium
|
||||
sandyaa - https://github.com/securelayer7/sandyaa
|
||||
|
||||
pipeline vuln tester, very comprehensive
|
||||
darwis-taka - https://hub.docker.com/r/cysecurity/darwis-taka
|
||||
@@ -0,0 +1,47 @@
|
||||
# for the pipeline
|
||||
## languages
|
||||
#### The tools we are using to write this in and deploy it
|
||||
helm
|
||||
pulumi
|
||||
argo workflows?
|
||||
|
||||
## pipeline
|
||||
#### The actual steps in the pipeline
|
||||
pulumi
|
||||
pulumi crossguard
|
||||
socket.dev
|
||||
argo workflows
|
||||
semgrep
|
||||
trufflehog
|
||||
syft // do we need this as socket.dev or semgrep can do sbom?
|
||||
grype
|
||||
renovate bot
|
||||
kics (keeping infrastructure as code secure)
|
||||
|
||||
## k8's
|
||||
#### Things I assume I need installed in my k8's cluster
|
||||
infisical
|
||||
argo workflows
|
||||
defectdojo
|
||||
|
||||
## repository
|
||||
#### Things to set on the repository
|
||||
branch protection
|
||||
|
||||
## local
|
||||
#### Things to add to my chezmoi install so that they are always available but should be mentioned as things the user should have
|
||||
eslint-plugin-security
|
||||
gitleaks
|
||||
socket cli
|
||||
|
||||
## Might be needed
|
||||
#### Things that we might need. I am unsure if we have other tools that sufficiently cover the security concerns
|
||||
trivy
|
||||
|
||||
# For homelab
|
||||
## optional things
|
||||
#### These are things that will exist in my homelab eventually, however they are not needed for this pipeline I think
|
||||
harbor containe registry
|
||||
suse security (neuvector)
|
||||
nexus package caching
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Improvement Plan: Refactor Infisical Secrets to Native CRD
|
||||
|
||||
## Objective
|
||||
The previous implementation used a Mutating Webhook (Infisical Agent Injector) and an `initContainer` polling loop to wait for secrets to be injected into the Argo Workflow pods. Best practices indicate this causes race conditions and ArgoCD "OutOfSync" issues. We need to refactor the pipeline to use the native `InfisicalSecret` CRD and standard Kubernetes `secretKeyRef` environment variables.
|
||||
|
||||
## Requirements
|
||||
- **Remove Webhook Logic**: Strip out any Infisical annotations (e.g., `secrets.infisical.com/auto-reload`) from the Argo Workflows pod metadata.
|
||||
- **Remove initContainer**: Delete the `initContainer` polling logic that was waiting for environment variables to populate.
|
||||
- **Create InfisicalSecret CRD**: Create a new Helm template (e.g., `helm/templates/infisical-secret.yaml`) defining an `InfisicalSecret` resource. This resource should sync the required secrets (Socket.dev API key, Pulumi credentials, S3/MinIO credentials, DefectDojo API keys) into a standard Kubernetes `Secret` (e.g., named `amp-security-pipeline-secrets`).
|
||||
- **Update Workflow Tasks**: Modify the `ClusterWorkflowTemplate` (and any other files where tasks are defined). Instead of expecting the webhook to inject the secrets directly, configure the task containers to pull their required environment variables using native Kubernetes syntax:
|
||||
```yaml
|
||||
env:
|
||||
- name: SOCKET_DEV_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
key: SOCKET_DEV_API_KEY
|
||||
```
|
||||
|
||||
## Agent Instructions
|
||||
1. Find and open the implemented `ClusterWorkflowTemplate` and task definition YAML files in `helm/templates/`.
|
||||
2. Find and remove all instances of the `initContainer` secret-waiting logic.
|
||||
3. Find and remove all Infisical mutating webhook annotations from the workflow/pod templates.
|
||||
4. Create a new file `helm/templates/infisical-secret.yaml` defining the `InfisicalSecret` CRD. Make sure it targets the necessary secrets for Socket.dev, Pulumi, Storage, and DefectDojo.
|
||||
5. Update the `scan-socketdev`, `scan-crossguard`, `upload-storage`, and `upload-defectdojo` tasks in the workflow template to use native `valueFrom: secretKeyRef` for their required environment variables, referencing the new native Kubernetes Secret.
|
||||
6. Verify the YAML is valid and clean.
|
||||
@@ -24,15 +24,15 @@ To achieve this, the architecture utilizes "Defense in Depth," split across seve
|
||||
|
||||
---
|
||||
|
||||
2. Part 1: Local Development & Repository Tooling
|
||||
2.1 Secret Scanning: Gitleaks (Local)
|
||||
## 2. Part 1: Local Development & Repository Tooling
|
||||
### 2.1 Secret Scanning: Gitleaks (Local)
|
||||
|
||||
What it does: Fast, static regex matching for secrets.
|
||||
Where it runs: Local developer machine (via Pre-commit hook).
|
||||
Detailed Rationale: Developers make human errors. Gitleaks runs in milliseconds and acts as a "spell-check for secrets." It prevents accidentally committing a .env file or hardcoded token before it ever enters the local Git history.
|
||||
Trade-offs: It relies on the developer actively using the pre-commit hook. If a commit is forced (--no-verify), the local check is bypassed.
|
||||
|
||||
2.2 Supply Chain Defense: Socket CLI (Local Wrapper)
|
||||
### 2.2 Supply Chain Defense: Socket CLI (Local Wrapper)
|
||||
|
||||
What it does: Intercepts package installation to check for malicious code, typosquatting, and hijacked packages.
|
||||
Where it runs: Local machine (aliased: alias pnpm="socket pnpm").
|
||||
@@ -62,6 +62,7 @@ To achieve this, the architecture utilizes "Defense in Depth," split across seve
|
||||
* **Detailed Rationale:** Traditional CVE scanners check for accidental developer mistakes. Socket checks for active malice (install scripts that steal SSH keys, typosquatting, hijacked maintainer accounts). Because AI agents regularly pull in new dependencies to solve coding problems, Socket ensures neither the local machine nor the pipeline executes malicious code during dependency resolution.
|
||||
* **Trade-offs:** API-dependent. To conserve free-tier API quotas, the pipeline step must be strictly configured to trigger *only* when lockfiles (`pnpm-lock.yaml`) change, requiring careful CI optimization.
|
||||
|
||||
**outdated, using pulumi crossguard**
|
||||
### 2.5 Infrastructure Validation (IaC): Checkov
|
||||
* **What it does:** Parses Kubernetes manifests, Terraform, and Dockerfiles to ensure they adhere to security best practices.
|
||||
* **Detailed Rationale:** A homelab exposed to the internet cannot afford basic infrastructure misconfigurations, such as running containers as `root` or mapping sensitive host volumes. Checkov acts as an automated senior cloud architect, validating the AI's generated Kubernetes manifests before Argo CD syncs them.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Implementation Plan: Base ClusterWorkflowTemplate
|
||||
|
||||
## Objective
|
||||
Create the foundational Argo `ClusterWorkflowTemplate` for the security pipeline. It must use semantic versioning (e.g., `amp-security-pipeline-v1.0.0`) so projects can pin to a stable version.
|
||||
|
||||
## Requirements
|
||||
- Define a `ClusterWorkflowTemplate` resource.
|
||||
- Name the template with a semver tag (e.g., `name: amp-security-pipeline-v1.0.0`).
|
||||
- Define inputs/parameters:
|
||||
- `working-dir` (default: `.`)
|
||||
- `fail-on-cvss` (default: `7.0`)
|
||||
- `repo-url` (required)
|
||||
- `git-revision` (default: `main`)
|
||||
- Define the DAG (Directed Acyclic Graph) structure that will orchestrate the phases (Clone -> Parallel Scanners -> Sinks/Enforcement).
|
||||
|
||||
## Agent Instructions
|
||||
1. Create `helm/templates/clusterworkflowtemplate.yaml`.
|
||||
2. Ensure the template is structured to accept the parameters and orchestrate downstream DAG tasks.
|
||||
3. Keep the actual task implementations (like git clone or scanners) as empty stubs for now; they will be filled by subsequent steps.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Implementation Plan: Shared PVC Workspace & Git Clone
|
||||
|
||||
## Objective
|
||||
Implement a shared Persistent Volume Claim (PVC) strategy to ensure the repository is only cloned once and all parallel scanners can access the same codebase without re-downloading it.
|
||||
|
||||
## Requirements
|
||||
- Use Argo Workflows `volumeClaimTemplates` to define a temporary PVC for the workflow duration.
|
||||
- Create a `clone-repo` task in the DAG.
|
||||
- The `clone-repo` task should use a standard git image (e.g., Alpine/Git) to clone the `repo-url` at `git-revision` into the shared PVC mounted at `/workspace`.
|
||||
- Ensure all subsequent tasks will mount this PVC at `/workspace`.
|
||||
|
||||
## Agent Instructions
|
||||
1. Modify the `ClusterWorkflowTemplate` to add the `volumeClaimTemplates`.
|
||||
2. Add the `clone-repo` task template that executes `git clone`.
|
||||
3. Configure the DAG so the parallel scanning steps depend on the successful completion of `clone-repo`.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Implementation Plan: Infisical Secrets Injection InitContainer
|
||||
|
||||
## Objective
|
||||
Ensure that Infisical secrets are injected as **Environment Variables** securely before any main container logic runs in the Argo Workflows steps.
|
||||
|
||||
## Requirements
|
||||
- Use the Infisical Kubernetes operator approach.
|
||||
- Add the necessary Infisical annotations (e.g., `secrets.infisical.com/auto-reload: "true"`) to the pod metadata templates.
|
||||
- **Crucial:** Because Argo Workflows pods start quickly, inject an `initContainer` into tasks that require secrets. This initContainer should run a simple polling script (e.g., a loop checking if a specific expected environment variable exists) to pause the pod's main container execution until the Infisical mutating webhook has successfully injected the environment variables.
|
||||
|
||||
## Agent Instructions
|
||||
1. Create a reusable snippet or template property for the `initContainer` wait logic.
|
||||
2. Apply the required Infisical annotations to the `ClusterWorkflowTemplate`'s `podSpecPatch` or task metadata.
|
||||
3. Document which steps will require which secrets (e.g., DefectDojo API keys, Socket.dev keys).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Implementation Plan: TruffleHog Scanner
|
||||
|
||||
## Objective
|
||||
Implement the TruffleHog secrets scanning step as a parallel task in the DAG.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `scan-trufflehog`.
|
||||
- Depend on the `clone-repo` task.
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Run TruffleHog against the `/workspace` directory.
|
||||
- Configure TruffleHog to output its findings in JSON or SARIF format.
|
||||
- Save the output to `/workspace/reports/trufflehog.json` (or `.sarif`).
|
||||
- Ensure the task exits successfully (exit code 0) even if secrets are found, so the pipeline can proceed to the aggregation step (Phase 3). (Use `continueOn` or `ignoreError` or a wrapper script like `trufflehog ... || true`).
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `scan-trufflehog` template to the `ClusterWorkflowTemplate`.
|
||||
2. Wire it into the DAG alongside the other scanners.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Implementation Plan: Semgrep Scanner
|
||||
|
||||
## Objective
|
||||
Implement the Semgrep SAST (Static Application Security Testing) scanning step as a parallel task in the DAG.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `scan-semgrep`.
|
||||
- Depend on the `clone-repo` task.
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Run Semgrep with standard or configurable rulesets against the `/workspace` directory.
|
||||
- Output findings in SARIF format.
|
||||
- Save the output to `/workspace/reports/semgrep.sarif`.
|
||||
- Ensure the task exits successfully even if vulnerabilities are found, so Phase 3 aggregation can run (e.g., wrap in a script that returns 0).
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `scan-semgrep` template to the `ClusterWorkflowTemplate`.
|
||||
2. Wire it into the DAG alongside the other scanners.
|
||||
3. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Implementation Plan: KICS IaC Scanner
|
||||
|
||||
## Objective
|
||||
Implement the KICS (Keeping Infrastructure as Code Secure) scanning step as a parallel task in the DAG.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `scan-kics`.
|
||||
- Depend on the `clone-repo` task.
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Run KICS against the `/workspace` directory (or the specific `working-dir` parameter).
|
||||
- Output findings in SARIF and/or JSON format.
|
||||
- Save the output to `/workspace/reports/kics.sarif`.
|
||||
- Ensure the task exits successfully even if issues are found, to allow Phase 3 aggregation (e.g., wrap with `|| true`).
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `scan-kics` template to the `ClusterWorkflowTemplate`.
|
||||
2. Wire it into the DAG alongside the other scanners.
|
||||
3. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Implementation Plan: Socket.dev Scanner
|
||||
|
||||
## Objective
|
||||
Implement the Socket.dev supply chain security scanning step as a parallel task in the DAG.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `scan-socketdev`.
|
||||
- Depend on the `clone-repo` task.
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Expect the Socket.dev API key to be injected via Infisical as an environment variable (use the initContainer wait logic from Phase 1 Step 3).
|
||||
- Run the Socket CLI against the dependency manifests in `/workspace`.
|
||||
- Output findings in a standard format (JSON/SARIF).
|
||||
- Save the output to `/workspace/reports/socketdev.json`.
|
||||
- Ensure the task exits successfully (e.g. `|| true`) to allow Phase 3 aggregation.
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `scan-socketdev` template to the `ClusterWorkflowTemplate`.
|
||||
2. Configure the Infisical initContainer logic for this specific step to wait for the API key.
|
||||
3. Wire it into the DAG alongside the other scanners.
|
||||
4. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Implementation Plan: Syft & Grype Scanner
|
||||
|
||||
## Objective
|
||||
Implement the SBOM generation (Syft) and vulnerability scanning (Grype) step as a parallel task in the DAG.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `scan-syft-grype`.
|
||||
- Depend on the `clone-repo` task.
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Step A: Run Syft against `/workspace` to generate an SBOM (SPDX/CycloneDX format) -> `/workspace/reports/sbom.json`.
|
||||
- Step B: Run Grype against the generated SBOM (or the workspace directly) to find vulnerabilities.
|
||||
- Output Grype findings in SARIF format.
|
||||
- Save the Grype output to `/workspace/reports/grype.sarif`.
|
||||
- Ensure the task exits successfully (`|| true`) to allow Phase 3 aggregation.
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `scan-syft-grype` template to the `ClusterWorkflowTemplate`.
|
||||
2. Wire it into the DAG alongside the other scanners.
|
||||
3. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Implementation Plan: Pulumi Crossguard
|
||||
|
||||
## Objective
|
||||
Implement the Pulumi Crossguard policy enforcement step as a parallel task in the DAG.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `scan-crossguard`.
|
||||
- Depend on the `clone-repo` task.
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Expect Pulumi credentials and cloud provider credentials (e.g., AWS/GCP) to be injected via Infisical as environment variables (using the initContainer logic).
|
||||
- Run `pulumi preview --policy-pack <path>` inside the `/workspace`.
|
||||
- Capture the output and convert/save it into a structured JSON/SARIF format at `/workspace/reports/crossguard.json`.
|
||||
- Ensure the task exits successfully (`|| true`) to allow Phase 3 aggregation.
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `scan-crossguard` template to the `ClusterWorkflowTemplate`.
|
||||
2. Configure the Infisical initContainer to wait for Pulumi and Cloud credentials.
|
||||
3. Wire it into the DAG alongside the other scanners.
|
||||
4. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Implementation Plan: Long-Term Storage Upload
|
||||
|
||||
## Objective
|
||||
Implement an aggregation task that uploads all generated reports from the PVC to long-term storage (e.g., S3/MinIO) for audit trails and historical review.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `upload-storage`.
|
||||
- Depend on the successful completion of **all** parallel scanner tasks (Phase 2).
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Expect S3/MinIO credentials to be injected as environment variables via Infisical (with initContainer wait logic).
|
||||
- Use a CLI (like `aws s3 cp` or `mc`) to sync the `/workspace/reports/` directory to a designated bucket, keyed by repository name, date, and commit hash.
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `upload-storage` template to the `ClusterWorkflowTemplate`.
|
||||
2. Configure the DAG dependencies so it waits for all scanners.
|
||||
3. Configure the Infisical initContainer to wait for the storage credentials.
|
||||
4. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Implementation Plan: DefectDojo Upload
|
||||
|
||||
## Objective
|
||||
Implement a task that pushes all SARIF/JSON reports from the PVC to DefectDojo via its API.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `upload-defectdojo`.
|
||||
- Depend on the completion of all parallel scanner tasks (Phase 2).
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Expect DefectDojo API keys and URL to be injected as environment variables via Infisical (with initContainer wait logic).
|
||||
- Iterate over the `/workspace/reports/` directory.
|
||||
- For each file, make an API request to DefectDojo to import the scan results (mapping the file type to the correct DefectDojo parser, e.g., SARIF -> Generic SARIF).
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `upload-defectdojo` template to the `ClusterWorkflowTemplate`.
|
||||
2. Write the API upload script (Python, curl, or a dedicated CLI) in the task template.
|
||||
3. Configure the Infisical initContainer to wait for the DefectDojo credentials.
|
||||
4. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Implementation Plan: Policy Enforcement
|
||||
|
||||
## Objective
|
||||
Implement the final task that parses the aggregated results and decides whether to Pass or Fail the Argo Workflow based on the `fail-on-cvss` input threshold.
|
||||
|
||||
## Requirements
|
||||
- Define a task template named `enforce-policy`.
|
||||
- Depend on the completion of the upload tasks (Phase 3 Steps 1 & 2).
|
||||
- Mount the shared PVC at `/workspace`.
|
||||
- Read the input parameter `fail-on-cvss` (e.g., `7.0`).
|
||||
- Run a script (Python, jq, etc.) to parse all the reports in `/workspace/reports/`.
|
||||
- If any vulnerability is found with a CVSS score >= the threshold, print an error summary and exit with a non-zero code (causing the Argo Workflow to fail).
|
||||
- If no vulnerabilities exceed the threshold, print a success summary and exit with 0.
|
||||
|
||||
## Agent Instructions
|
||||
1. Add the `enforce-policy` template to the `ClusterWorkflowTemplate`.
|
||||
2. Write the parsing logic inside the task (e.g., extracting CVSS scores from SARIF and JSON formats).
|
||||
3. Ensure this step acts as the final gatekeeper for the pipeline.
|
||||
4. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Implementation Plan: Renovate Bot Preset
|
||||
|
||||
## Objective
|
||||
Create a centralized `renovate.json` (or `default.json`) preset in this repository that other projects can easily inherit to get standardized auto-merge and grouping behavior.
|
||||
|
||||
## Requirements
|
||||
- Create a file at `renovate-preset/default.json` (or similar path).
|
||||
- Configure auto-merge for patch and minor versions of dependencies.
|
||||
- Enable grouping for monorepo packages (e.g., all `@babel/*` updates grouped into one PR).
|
||||
- Configure the schedule (e.g., run on weekends or early mornings).
|
||||
- Configure the severity levels for when notifications/PRs should block.
|
||||
- Document how other repositories can `extend` this preset in their own `renovate.json` (e.g., `"extends": ["github>my-org/my-repo//renovate-preset"]`).
|
||||
|
||||
## Agent Instructions
|
||||
1. Create the base Renovate configuration file.
|
||||
2. Add a `README.md` to the `renovate-preset` directory explaining how to use it.
|
||||
3. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your JSON configurations or manifests into separate, smaller files to prevent exhausting the context window.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Implementation Plan: Renovate Bot CronJob / ArgoCD App
|
||||
|
||||
## Objective
|
||||
Create the Kubernetes manifests to deploy Renovate Bot as a cluster-level service (CronJob) via ArgoCD, configured to scan repositories and open PRs (which will trigger the Phase 1-3 pipeline).
|
||||
|
||||
## Requirements
|
||||
- Create Kubernetes manifests for a CronJob that runs the Renovate Bot Docker image.
|
||||
- Expect Git Provider credentials (GitHub/GitLab token) to be injected as environment variables via Infisical (using standard operator annotations).
|
||||
- Configure the CronJob to run periodically (e.g., hourly).
|
||||
- Package this as an ArgoCD Application or a Helm chart located in `helm/renovate-bot/`.
|
||||
- The configuration should instruct Renovate to scan the designated repositories and respect the presets defined in Phase 4 Step 1.
|
||||
|
||||
## Agent Instructions
|
||||
1. Create the `helm/renovate-bot` directory.
|
||||
2. Add the `CronJob`, `ServiceAccount`, and necessary RBAC manifests.
|
||||
3. Configure the Infisical annotations for secrets injection.
|
||||
4. Provide an `Application` manifest for ArgoCD to deploy it easily.
|
||||
5. **CRITICAL: File Splitting:** Do NOT put everything into one giant file! Split your YAML manifests or configurations into separate, smaller files (e.g. using separate Helm template files, configmaps, or helper scripts) to prevent exhausting the context window.
|
||||
@@ -0,0 +1,268 @@
|
||||
# Rendered reference for the default chart values in helm/values.yaml.
|
||||
# This is intended for reading and review, so you can inspect the final Argo object
|
||||
# without also mentally evaluating Helm templates.
|
||||
#
|
||||
# Notes:
|
||||
# - Optional storage, DefectDojo, and Infisical resources are omitted because they are
|
||||
# disabled by default.
|
||||
# - Argo placeholders such as {{workflow.parameters.repo-url}} are expected and remain
|
||||
# in the rendered object because Argo resolves them at workflow runtime.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ClusterWorkflowTemplate
|
||||
metadata:
|
||||
name: amp-security-pipeline-v1.0.0
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
entrypoint: security-pipeline
|
||||
onExit: pipeline-exit-hook
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: workspace
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "."
|
||||
- name: fail-on-cvss
|
||||
value: "7.0"
|
||||
- name: repo-url
|
||||
- name: git-revision
|
||||
value: "main"
|
||||
templates:
|
||||
- name: security-pipeline
|
||||
dag:
|
||||
tasks:
|
||||
- name: clone
|
||||
template: clone-repo
|
||||
arguments:
|
||||
parameters:
|
||||
- name: repo-url
|
||||
value: "{{workflow.parameters.repo-url}}"
|
||||
- name: git-revision
|
||||
value: "{{workflow.parameters.git-revision}}"
|
||||
- name: scanners
|
||||
dependencies:
|
||||
- clone
|
||||
template: parallel-scanners
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{workflow.parameters.working-dir}}"
|
||||
- name: enforce-policy
|
||||
dependencies:
|
||||
- scanners
|
||||
template: enforce-policy
|
||||
arguments:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
value: "{{workflow.parameters.fail-on-cvss}}"
|
||||
- name: clone-repo
|
||||
inputs:
|
||||
parameters:
|
||||
- name: repo-url
|
||||
- name: git-revision
|
||||
container:
|
||||
image: alpine/git:2.45.2
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- git clone --branch "{{inputs.parameters.git-revision}}" --single-branch "{{inputs.parameters.repo-url}}" /workspace
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: parallel-scanners
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
dag:
|
||||
tasks:
|
||||
- name: trufflehog
|
||||
template: scan-trufflehog
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
- name: semgrep
|
||||
template: scan-semgrep
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
- name: kics
|
||||
template: scan-kics
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
- name: socketdev
|
||||
template: scan-socketdev
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
- name: syft-grype
|
||||
template: scan-syft-grype
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
- name: pulumi-crossguard
|
||||
template: scan-pulumi-crossguard
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: "{{inputs.parameters.working-dir}}"
|
||||
- name: pipeline-exit-hook
|
||||
container:
|
||||
image: curlimages/curl:8.8.0
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
echo "Pipeline completed with status: {{workflow.status}}"
|
||||
- name: scan-trufflehog
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: trufflesecurity/trufflehog:latest
|
||||
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
|
||||
- name: scan-semgrep
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
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/{{inputs.parameters.working-dir}}" || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: scan-kics
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: checkmarx/kics:1.7.14
|
||||
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
|
||||
- name: scan-socketdev
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
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/{{inputs.parameters.working-dir}}" --format json --output /workspace/reports/socketdev.json || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: scan-syft-grype
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: anchore/syft:latest
|
||||
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
|
||||
- name: scan-pulumi-crossguard
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: pulumi/pulumi:3.154.0
|
||||
env:
|
||||
- name: PULUMI_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
key: PULUMI_ACCESS_TOKEN
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
cd "/workspace/{{inputs.parameters.working-dir}}"
|
||||
pulumi preview --policy-pack "policy-pack" > /workspace/reports/pulumi-crossguard.json 2>&1 || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: enforce-policy
|
||||
inputs:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
container:
|
||||
image: agentguard-tools:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- node
|
||||
- /app/dist/enforce-policy.js
|
||||
env:
|
||||
- name: FAIL_ON_CVSS
|
||||
value: "{{inputs.parameters.fail-on-cvss}}"
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
@@ -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)
|
||||
* **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).
|
||||
* **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
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# Security Scope and Boundaries
|
||||
|
||||
This repository is intentionally a **source, IaC, and dependency scanning pipeline**.
|
||||
It is not a complete software supply chain or runtime security platform.
|
||||
|
||||
The goal of this document is to make the scope explicit so users understand:
|
||||
|
||||
- what this repository is designed to secure
|
||||
- what it does not secure
|
||||
- what additional controls are still required for a production or homelab deployment pipeline
|
||||
- what requirements a sibling pipeline should meet if you want to fill the gaps
|
||||
|
||||
## What this repository covers
|
||||
|
||||
This repository provides a reusable pre-deploy security gate that scans a repository before deployment proceeds.
|
||||
|
||||
Today it covers:
|
||||
|
||||
- **Secret scanning** with TruffleHog in the pipeline and Gitleaks in local development guidance.
|
||||
- **First-party code scanning** with Semgrep.
|
||||
- **Infrastructure-as-code scanning** with KICS.
|
||||
- **Dependency risk scanning** with Socket.dev.
|
||||
- **SBOM generation and vulnerability scanning** with Syft and Grype.
|
||||
- **Policy-pack validation** with Pulumi CrossGuard.
|
||||
- **Finding export and triage plumbing** through optional DefectDojo and object storage uploads.
|
||||
- **Dependency update automation support** through the Renovate preset and Renovate bot chart in this repository.
|
||||
|
||||
In practical terms, this repository is strongest at:
|
||||
|
||||
- stopping known-bad or suspicious changes before deploy
|
||||
- reducing the chance that AI-generated insecure code or dependencies reach production
|
||||
- creating visibility into code, IaC, dependency, and package vulnerability risk
|
||||
|
||||
## What this repository does not cover
|
||||
|
||||
This repository does **not** attempt to be responsible for every security control in a modern delivery system.
|
||||
|
||||
It does not provide:
|
||||
|
||||
- **Artifact signing** for container images, packages, or release bundles.
|
||||
- **Build provenance / attestations** proving which trusted builder produced an artifact from which source revision.
|
||||
- **Admission control verification** that only signed or attested artifacts may deploy.
|
||||
- **Runtime threat detection** inside the cluster or host.
|
||||
- **Network segmentation or runtime isolation** for the deployed applications.
|
||||
- **DAST** or other black-box testing against a live running application.
|
||||
- **Repository hosting configuration enforcement** such as branch protection, required reviews, CODEOWNERS, or token restrictions.
|
||||
- **Application deployment hardening enforcement** such as checking whether deployed workloads run as non-root, drop capabilities, use read-only root filesystems, or have network policies.
|
||||
- **Secure secret delivery for the scanned application** beyond the pipeline's own runtime secret needs.
|
||||
- **Incident response, rollback, backup, disaster recovery, or patch SLAs**.
|
||||
|
||||
Those areas still matter. They are simply outside the intended scope of this repository.
|
||||
|
||||
## Why the scope stops here
|
||||
|
||||
The security boundary for this repository is:
|
||||
|
||||
1. clone a source repository
|
||||
2. scan source, IaC, dependencies, and generated SBOM data
|
||||
3. publish findings
|
||||
4. fail or pass based on policy
|
||||
|
||||
That boundary is useful because it keeps this repository focused, fast, and reusable.
|
||||
If artifact build security, deploy admission, and runtime controls are mixed directly into this pipeline, the repository stops being a clear pre-deploy scanner and turns into a broader platform.
|
||||
|
||||
This repository should remain the **shift-left scanning gate**, not the entire delivery architecture.
|
||||
|
||||
## What a complete deployment pipeline still needs
|
||||
|
||||
If you are deploying software after this pipeline passes, you still need additional controls elsewhere.
|
||||
|
||||
At a minimum, a production-grade or public homelab deployment flow should also have:
|
||||
|
||||
### 1. Repository and change-management controls
|
||||
|
||||
These controls protect the trustworthiness of the source before scanning even begins.
|
||||
|
||||
Recommended controls:
|
||||
|
||||
- branch protection on default and release branches
|
||||
- required pull request reviews for sensitive paths
|
||||
- `CODEOWNERS` for pipeline, deployment, and policy files
|
||||
- restricted direct pushes to protected branches
|
||||
- minimal repository admin access
|
||||
- mandatory CI status checks before merge
|
||||
- protected tags or release branches if releases are tag-driven
|
||||
|
||||
This repository can document those requirements, but enforcement belongs in the source control platform.
|
||||
|
||||
### 2. Build pipeline controls
|
||||
|
||||
These controls establish that build outputs came from a trusted process.
|
||||
|
||||
Recommended controls:
|
||||
|
||||
- isolated build environment
|
||||
- pinned builder images and actions
|
||||
- least-privilege credentials for the build system
|
||||
- artifact signing for produced container images or release assets
|
||||
- provenance / attestation generation for builds
|
||||
- immutable artifact storage or registry retention controls
|
||||
|
||||
If you want image signing or provenance, that should usually live in the **build pipeline**, because that is where artifacts are actually created.
|
||||
|
||||
### 3. Deploy pipeline controls
|
||||
|
||||
These controls ensure only approved artifacts and manifests reach the cluster.
|
||||
|
||||
Recommended controls:
|
||||
|
||||
- verification of image signatures or attestations before deploy
|
||||
- enforcement that deployment pulls only from approved registries
|
||||
- promotion between environments using immutable digests rather than floating tags
|
||||
- deployment-time policy checks for manifests and workload security settings
|
||||
- manual approval or staged rollout where risk justifies it
|
||||
|
||||
### 4. Runtime and cluster controls
|
||||
|
||||
These controls reduce blast radius after deployment.
|
||||
|
||||
Recommended controls:
|
||||
|
||||
- namespace isolation and least-privilege RBAC
|
||||
- workload security contexts
|
||||
- non-root containers where possible
|
||||
- dropped Linux capabilities
|
||||
- read-only root filesystems where possible
|
||||
- Kubernetes network policies
|
||||
- admission control for workload standards
|
||||
- runtime threat detection / alerting
|
||||
- centralized logs, metrics, and alerts
|
||||
- secrets delivery from a vault or equivalent system
|
||||
|
||||
### 5. Recovery and resilience controls
|
||||
|
||||
These controls matter because modern guidance assumes prevention will sometimes fail.
|
||||
|
||||
Recommended controls:
|
||||
|
||||
- tested rollback procedures
|
||||
- backups and restore testing
|
||||
- credential rotation procedures
|
||||
- vulnerability remediation SLAs
|
||||
- incident response playbooks
|
||||
- inventory of critical services and owners
|
||||
|
||||
## Recommended sibling pipeline responsibilities
|
||||
|
||||
If you build a second pipeline to fill the gaps, keep the separation of concerns explicit.
|
||||
|
||||
A sibling **build and deploy security pipeline** should be responsible for:
|
||||
|
||||
- building artifacts in a trusted environment
|
||||
- generating SBOMs tied to the built artifact
|
||||
- signing artifacts
|
||||
- generating provenance / attestations
|
||||
- verifying signatures or attestations before deploy
|
||||
- enforcing deploy-time policy on manifests and workloads
|
||||
- optionally running DAST or post-deploy validation outside the fast PR loop
|
||||
|
||||
That keeps this repository focused on source scanning while the sibling pipeline owns artifact trust and deployment trust.
|
||||
|
||||
## Mapping this repository to modern security guidance
|
||||
|
||||
A concise way to think about the split is:
|
||||
|
||||
- **Assume compromise:** this repo helps by enforcing checks before deploy, but recovery and runtime containment live elsewhere.
|
||||
- **Defense in depth:** this repo is one layer, not the whole system.
|
||||
- **Blast-radius reduction:** this repo can reduce risky changes reaching production, but runtime RBAC, isolation, and network controls are outside its scope.
|
||||
- **Zero trust / verified supply chain:** this repo contributes source and dependency scrutiny, but full artifact trust also needs build signing and deploy verification in another pipeline.
|
||||
- **Prevention plus detection and recovery:** this repo is primarily a prevention and early-detection layer.
|
||||
|
||||
## Minimum expectations for users of this repository
|
||||
|
||||
If you adopt this repository, you should assume it is only one part of the security architecture.
|
||||
|
||||
At minimum, you should also have:
|
||||
|
||||
- repository branch protection and required reviews
|
||||
- a secure build pipeline
|
||||
- a deployment path that uses immutable artifacts
|
||||
- workload hardening standards for deployed applications
|
||||
- secret management outside Git
|
||||
- logging/monitoring and a plan for response when a scanner finds something important
|
||||
|
||||
## Non-goals
|
||||
|
||||
To avoid scope creep, the following are non-goals for this repository:
|
||||
|
||||
- becoming the authoritative runtime security platform
|
||||
- becoming the artifact signing system
|
||||
- becoming the deployment admission controller
|
||||
- replacing source control platform protections
|
||||
- replacing cluster hardening or incident response processes
|
||||
|
||||
## Summary
|
||||
|
||||
This repository is the **source-side security gate**.
|
||||
It helps answer: "Is this code, IaC, dependency set, and generated SBOM safe enough to continue?"
|
||||
|
||||
It does **not** answer the full downstream questions:
|
||||
|
||||
- "Was the artifact built by a trusted builder?"
|
||||
- "Should the cluster admit this artifact?"
|
||||
- "Is the running workload isolated and monitored correctly?"
|
||||
- "Can we detect, contain, and recover if prevention fails?"
|
||||
|
||||
Those controls should exist, but they should live in adjacent repository, build, deploy, and runtime systems rather than being forced into this repository.
|
||||
@@ -23,6 +23,7 @@ To maintain developer velocity (the "Friction" principle), pipeline feedback mus
|
||||
* **Tool:** `eslint` with `eslint-plugin-security` and `@typescript-eslint`.
|
||||
* **Reasoning:** Linters are "dumb" but instantaneous. They will catch AI agents generating immediately dangerous syntax (like `eval()` or unsafe Regex) before a commit is even made.
|
||||
|
||||
**outdated, using pulumi crossguard**
|
||||
### Layer 2: Infrastructure as Code (IaC) Scanning
|
||||
* **Tool:** Checkov (Open Source)
|
||||
* **Reasoning:** Lightweight CLI tool to ensure the AI agents do not accidentally expose internal homelab ports to the internet or misconfigure container permissions.
|
||||
@@ -47,6 +48,7 @@ To maintain developer velocity (the "Friction" principle), pipeline feedback mus
|
||||
| **Snyk Code** | Great UX, but lacks the ability to write custom rules. If the AI agent develops a specific bad habit unique to this codebase, Snyk cannot be easily tuned to block it. |
|
||||
| **Checkmarx / Veracode** | Built for massive legacy enterprise compliance. Far too expensive, slow, and noisy for a modern, agile homelab setup. |
|
||||
|
||||
**outdated using harvester default registry**
|
||||
## 5. Future Considerations / Phase 2
|
||||
* **Build Caching:** If actual container build steps (`docker build`, `npm install`) become the bottleneck in Argo Workflows, evaluate adding open-source caching layers like **Kaniko** or **BuildKit** inside Argo pods before purchasing paid caching solutions.
|
||||
* **Custom Semgrep Rules:** If the AI agent repeatedly makes domain-specific logic errors (e.g., misusing a specific custom Monad), write lightweight custom Semgrep YAML rules to permanently block those specific anti-patterns.
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: renovate-bot
|
||||
description: Renovate Bot deployment for agentguard-ci
|
||||
version: 0.1.0
|
||||
appVersion: "37.0.0"
|
||||
@@ -0,0 +1,17 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: renovate-bot
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://git.example.com/agentguard-ci.git
|
||||
targetRevision: main
|
||||
path: helm/renovate-bot
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: default
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: renovate-bot
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets", "configmaps"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: renovate-bot
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: renovate-bot
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: renovate-bot
|
||||
namespace: default
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: renovate-bot-config
|
||||
data:
|
||||
renovate.json: |
|
||||
{
|
||||
"extends": [{{ .Values.preset | quote }}],
|
||||
"onboarding": false,
|
||||
"platform": "github",
|
||||
"repositories": {{ toJson .Values.repositories }}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: renovate-bot
|
||||
spec:
|
||||
schedule: {{ .Values.schedule | quote }}
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
serviceAccountName: renovate-bot
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: renovate
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: RENOVATE_CONFIG_FILE
|
||||
value: /etc/renovate/renovate.json
|
||||
- name: RENOVATE_REPOSITORIES
|
||||
value: {{ join "," .Values.repositories | quote }}
|
||||
- name: GITHUB_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: renovate-bot
|
||||
key: github-token
|
||||
- name: GITLAB_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: renovate-bot
|
||||
key: gitlab-token
|
||||
args:
|
||||
- renovate
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/renovate
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: renovate-bot-config
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: renovate-bot
|
||||
annotations:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
@@ -0,0 +1,8 @@
|
||||
image:
|
||||
repository: renovate/renovate
|
||||
tag: 37.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
schedule: "0 * * * *"
|
||||
preset: "github>my-org/my-repo//renovate-preset"
|
||||
repositories: []
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- define "template.enforce-policy" -}}
|
||||
- name: enforce-policy
|
||||
inputs:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
container:
|
||||
image: {{ include "template.tools-image" . | quote }}
|
||||
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 }}
|
||||
@@ -0,0 +1,3 @@
|
||||
{{- define "template.tools-image" -}}
|
||||
{{- printf "%s:%s" .Values.pipeline.toolsImage.repository .Values.pipeline.toolsImage.tag -}}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{- define "template.scan-kics" -}}
|
||||
- name: scan-kics
|
||||
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,26 @@
|
||||
{{- define "template.scan-pulumi-crossguard" -}}
|
||||
- name: scan-pulumi-crossguard
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
container:
|
||||
image: {{ .Values.images.pulumiCrossguard }}
|
||||
env:
|
||||
- name: PULUMI_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
key: PULUMI_ACCESS_TOKEN
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
mkdir -p /workspace/reports
|
||||
cd "/workspace/{{ `{{inputs.parameters.working-dir}}` }}"
|
||||
pulumi preview --policy-pack {{ .Values.pulumi.policyPackPath | quote }} > /workspace/reports/pulumi-crossguard.json 2>&1 || true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- define "template.scan-semgrep" -}}
|
||||
- name: scan-semgrep
|
||||
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 }}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{- define "template.scan-socketdev" -}}
|
||||
- name: scan-socketdev
|
||||
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 }}
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- define "template.scan-syft-grype" -}}
|
||||
- name: scan-syft-grype
|
||||
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 }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- define "template.scan-trufflehog" -}}
|
||||
- name: scan-trufflehog
|
||||
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 }}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{- define "template.upload-defectdojo" -}}
|
||||
- name: upload-defectdojo
|
||||
container:
|
||||
image: {{ include "template.tools-image" . | quote }}
|
||||
imagePullPolicy: {{ .Values.pipeline.toolsImage.pullPolicy }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
env:
|
||||
- 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 }}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{- define "template.upload-storage" -}}
|
||||
- name: upload-storage
|
||||
container:
|
||||
image: {{ .Values.images.awsCli }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: amp-security-pipeline-secrets
|
||||
env:
|
||||
- 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
|
||||
export AWS_ACCESS_KEY_ID="${S3_ACCESS_KEY_ID:-}"
|
||||
export AWS_SECRET_ACCESS_KEY="${S3_SECRET_ACCESS_KEY:-}"
|
||||
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 }}
|
||||
@@ -0,0 +1,64 @@
|
||||
{{- define "template.workflow.security-pipeline.tasks" -}}
|
||||
- name: clone
|
||||
template: clone-repo
|
||||
arguments:
|
||||
parameters:
|
||||
- name: repo-url
|
||||
value: {{ `{{workflow.parameters.repo-url}}` | quote }}
|
||||
- name: git-revision
|
||||
value: {{ `{{workflow.parameters.git-revision}}` | quote }}
|
||||
- name: scanners
|
||||
dependencies:
|
||||
- clone
|
||||
template: parallel-scanners
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: {{ `{{workflow.parameters.working-dir}}` | quote }}
|
||||
- name: enforce-policy
|
||||
dependencies:
|
||||
- scanners
|
||||
template: enforce-policy
|
||||
arguments:
|
||||
parameters:
|
||||
- name: fail-on-cvss
|
||||
value: {{ `{{workflow.parameters.fail-on-cvss}}` | quote }}
|
||||
{{- if .Values.storage.enabled }}
|
||||
- name: upload-storage
|
||||
dependencies:
|
||||
- scanners
|
||||
template: upload-storage
|
||||
{{- end }}
|
||||
{{- if .Values.defectdojo.enabled }}
|
||||
- name: upload-defectdojo
|
||||
dependencies:
|
||||
- scanners
|
||||
template: upload-defectdojo
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "template.workflow.parallel-scanners.tasks" -}}
|
||||
{{- /* Scanner fan-out is data-driven from pipeline.scanners in values.yaml. */ -}}
|
||||
{{- range $scanner := .Values.pipeline.scanners }}
|
||||
- name: {{ $scanner }}
|
||||
template: scan-{{ $scanner }}
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: {{ `{{inputs.parameters.working-dir}}` | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "template.workflow.named-templates" -}}
|
||||
{{- /* Keep the main workflow file focused on orchestration; implementations are included here. */ -}}
|
||||
{{- range $scanner := .Values.pipeline.scanners }}
|
||||
{{ include (printf "template.scan-%s" $scanner) $ }}
|
||||
{{- end }}
|
||||
{{- if .Values.storage.enabled }}
|
||||
{{ include "template.upload-storage" . }}
|
||||
{{- end }}
|
||||
{{- if .Values.defectdojo.enabled }}
|
||||
{{ include "template.upload-defectdojo" . }}
|
||||
{{- end }}
|
||||
{{ include "template.enforce-policy" . }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,71 @@
|
||||
{{- if .Values.pipeline.enabled }}
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ClusterWorkflowTemplate
|
||||
metadata:
|
||||
name: {{ .Values.pipeline.name }}
|
||||
spec:
|
||||
serviceAccountName: {{ .Values.pipeline.serviceAccountName }}
|
||||
entrypoint: security-pipeline
|
||||
onExit: pipeline-exit-hook
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: workspace
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.pipeline.workspace.storage }}
|
||||
arguments:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
value: {{ .Values.pipeline.workingDir | quote }}
|
||||
- name: fail-on-cvss
|
||||
value: {{ .Values.pipeline.failOnCvss | quote }}
|
||||
- name: repo-url
|
||||
- name: git-revision
|
||||
value: {{ .Values.pipeline.gitRevision | quote }}
|
||||
templates:
|
||||
# Top-level DAG wiring lives here so the workflow flow stays readable.
|
||||
- name: security-pipeline
|
||||
dag:
|
||||
tasks:
|
||||
{{ include "template.workflow.security-pipeline.tasks" . | nindent 10 }}
|
||||
|
||||
# Concrete task implementations stay below.
|
||||
- name: clone-repo
|
||||
inputs:
|
||||
parameters:
|
||||
- name: repo-url
|
||||
- name: git-revision
|
||||
container:
|
||||
image: {{ .Values.images.git }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- git clone --branch {{ `{{inputs.parameters.git-revision}}` | quote }} --single-branch {{ `{{inputs.parameters.repo-url}}` | quote }} /workspace
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
|
||||
- name: parallel-scanners
|
||||
inputs:
|
||||
parameters:
|
||||
- name: working-dir
|
||||
dag:
|
||||
tasks:
|
||||
{{ include "template.workflow.parallel-scanners.tasks" . | nindent 10 }}
|
||||
|
||||
- name: pipeline-exit-hook
|
||||
container:
|
||||
image: {{ .Values.images.curl }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
echo "Pipeline completed with status: {{ `{{workflow.status}}` }}"
|
||||
{{ include "template.workflow.named-templates" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{- if and .Values.pipeline.enabled .Values.infisical.enabled }}
|
||||
apiVersion: infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
name: amp-security-pipeline-secrets
|
||||
spec:
|
||||
secretName: amp-security-pipeline-secrets
|
||||
target:
|
||||
creationPolicy: Owner
|
||||
workspaceSlug: {{ .Values.infisical.workspaceSlug | quote }}
|
||||
projectSlug: {{ .Values.infisical.projectSlug | quote }}
|
||||
secrets:
|
||||
- secretKey: SOCKET_DEV_API_KEY
|
||||
remoteRef:
|
||||
key: SOCKET_DEV_API_KEY
|
||||
- secretKey: PULUMI_ACCESS_TOKEN
|
||||
remoteRef:
|
||||
key: PULUMI_ACCESS_TOKEN
|
||||
- secretKey: S3_ACCESS_KEY_ID
|
||||
remoteRef:
|
||||
key: S3_ACCESS_KEY_ID
|
||||
- secretKey: S3_SECRET_ACCESS_KEY
|
||||
remoteRef:
|
||||
key: S3_SECRET_ACCESS_KEY
|
||||
- secretKey: DEFECTDOJO_URL
|
||||
remoteRef:
|
||||
key: DEFECTDOJO_URL
|
||||
- secretKey: DEFECTDOJO_API_TOKEN
|
||||
remoteRef:
|
||||
key: DEFECTDOJO_API_TOKEN
|
||||
{{- end }}
|
||||
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pipeline": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Core Argo workflow settings.",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Render the ClusterWorkflowTemplate when true."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ClusterWorkflowTemplate resource.",
|
||||
"minLength": 1
|
||||
},
|
||||
"serviceAccountName": {
|
||||
"type": "string",
|
||||
"description": "Service account used by workflow pods.",
|
||||
"minLength": 1
|
||||
},
|
||||
"workingDir": {
|
||||
"type": "string",
|
||||
"description": "Repository path scanned inside the cloned workspace.",
|
||||
"minLength": 1
|
||||
},
|
||||
"gitRevision": {
|
||||
"type": "string",
|
||||
"description": "Default git revision to clone when the workflow caller does not override it.",
|
||||
"minLength": 1
|
||||
},
|
||||
"failOnCvss": {
|
||||
"type": "string",
|
||||
"description": "CVSS threshold passed to the policy enforcement utility.",
|
||||
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "PVC configuration for the shared workspace volume.",
|
||||
"properties": {
|
||||
"storage": {
|
||||
"type": "string",
|
||||
"description": "Requested workspace PVC size, for example 1Gi.",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"storage"
|
||||
]
|
||||
},
|
||||
"repoName": {
|
||||
"type": "string",
|
||||
"description": "Repository name used in storage upload paths.",
|
||||
"minLength": 1
|
||||
},
|
||||
"scanners": {
|
||||
"type": "array",
|
||||
"description": "Ordered list of scanner templates wired into the scanner fan-out DAG.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"trufflehog",
|
||||
"semgrep",
|
||||
"kics",
|
||||
"socketdev",
|
||||
"syft-grype",
|
||||
"pulumi-crossguard"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"toolsImage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Custom image that packages the Node utilities used by the workflow.",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Always",
|
||||
"IfNotPresent",
|
||||
"Never"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"repository",
|
||||
"tag",
|
||||
"pullPolicy"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"name",
|
||||
"serviceAccountName",
|
||||
"workingDir",
|
||||
"gitRevision",
|
||||
"failOnCvss",
|
||||
"workspace",
|
||||
"repoName",
|
||||
"scanners",
|
||||
"toolsImage"
|
||||
]
|
||||
},
|
||||
"images": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Container images used by each workflow step.",
|
||||
"properties": {
|
||||
"git": { "type": "string", "minLength": 1 },
|
||||
"trufflehog": { "type": "string", "minLength": 1 },
|
||||
"semgrep": { "type": "string", "minLength": 1 },
|
||||
"kics": { "type": "string", "minLength": 1 },
|
||||
"socketdev": { "type": "string", "minLength": 1 },
|
||||
"syftGrype": { "type": "string", "minLength": 1 },
|
||||
"pulumiCrossguard": { "type": "string", "minLength": 1 },
|
||||
"awsCli": { "type": "string", "minLength": 1 },
|
||||
"curl": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": [
|
||||
"git",
|
||||
"trufflehog",
|
||||
"semgrep",
|
||||
"kics",
|
||||
"socketdev",
|
||||
"syftGrype",
|
||||
"pulumiCrossguard",
|
||||
"awsCli",
|
||||
"curl"
|
||||
]
|
||||
},
|
||||
"storage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Optional raw report upload configuration.",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reportsBucket": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"description": "Optional custom S3 endpoint for MinIO or another compatible store."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"reportsBucket",
|
||||
"endpoint"
|
||||
]
|
||||
},
|
||||
"pulumi": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Pulumi CrossGuard scanner settings.",
|
||||
"properties": {
|
||||
"policyPackPath": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"policyPackPath"
|
||||
]
|
||||
},
|
||||
"defectdojo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Optional DefectDojo upload step configuration.",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"productTypeName": { "type": "string", "minLength": 1 },
|
||||
"productName": { "type": "string", "minLength": 1 },
|
||||
"engagementName": { "type": "string", "minLength": 1 },
|
||||
"minimumSeverity": { "type": "string", "minLength": 1 },
|
||||
"active": { "type": "boolean" },
|
||||
"verified": { "type": "boolean" },
|
||||
"closeOldFindings": { "type": "boolean" },
|
||||
"autoCreateContext": { "type": "boolean" }
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"productTypeName",
|
||||
"productName",
|
||||
"engagementName",
|
||||
"minimumSeverity",
|
||||
"active",
|
||||
"verified",
|
||||
"closeOldFindings",
|
||||
"autoCreateContext"
|
||||
]
|
||||
},
|
||||
"infisical": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Optional Infisical operator integration.",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"workspaceSlug": { "type": "string" },
|
||||
"projectSlug": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"workspaceSlug",
|
||||
"projectSlug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pipeline",
|
||||
"images",
|
||||
"storage",
|
||||
"pulumi",
|
||||
"defectdojo",
|
||||
"infisical"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
pipeline:
|
||||
enabled: true
|
||||
name: amp-security-pipeline-v1.0.0
|
||||
serviceAccountName: default
|
||||
workingDir: .
|
||||
gitRevision: main
|
||||
failOnCvss: "7.0"
|
||||
workspace:
|
||||
storage: 1Gi
|
||||
repoName: agentguard-ci
|
||||
# Order here matches the scanner fan-out in the workflow DAG.
|
||||
scanners:
|
||||
- trufflehog
|
||||
- semgrep
|
||||
- kics
|
||||
- socketdev
|
||||
- syft-grype
|
||||
- pulumi-crossguard
|
||||
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: ""
|
||||
@@ -0,0 +1,22 @@
|
||||
# Renovate Preset
|
||||
|
||||
This directory contains a shared Renovate preset that other repositories can extend.
|
||||
|
||||
## Usage
|
||||
|
||||
In another repository's `renovate.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": ["github>my-org/my-repo//renovate-preset"]
|
||||
}
|
||||
```
|
||||
|
||||
Adjust `my-org/my-repo` to point at this repository.
|
||||
|
||||
## Behavior
|
||||
|
||||
- Auto-merges patch and minor updates.
|
||||
- Groups common monorepo package families into single PRs.
|
||||
- Schedules Renovate runs on weekends before 6am UTC.
|
||||
- Keeps security alerts from auto-merging.
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"timezone": "UTC",
|
||||
"schedule": ["before 6am on saturday", "before 6am on sunday"],
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"automergeStrategy": "squash",
|
||||
"automergeSchedule": ["before 6am on saturday", "before 6am on sunday"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["patch", "minor"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^@babel/"],
|
||||
"groupName": "babel packages"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^eslint"],
|
||||
"groupName": "eslint packages"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^jest"],
|
||||
"groupName": "jest packages"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^@types/"],
|
||||
"groupName": "types packages"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^react", "^react-dom"],
|
||||
"groupName": "react packages"
|
||||
},
|
||||
{
|
||||
"matchConfidence": ["high", "very-high"],
|
||||
"dependencyDashboardApproval": false
|
||||
},
|
||||
{
|
||||
"matchConfidence": ["low", "neutral"],
|
||||
"dependencyDashboardApproval": true
|
||||
}
|
||||
],
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["security"],
|
||||
"automerge": false
|
||||
}
|
||||
}
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
chart_dir="${repo_root}/helm"
|
||||
rendered_manifest="$(mktemp --suffix=.yaml)"
|
||||
release_name="${RELEASE_NAME:-agentguard-ci}"
|
||||
|
||||
cleanup() {
|
||||
rm -f "${rendered_manifest}"
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
printf 'Missing required command: %s\n' "$1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_kubectl_client_check() {
|
||||
require_command kubectl
|
||||
if ! kubectl apply --dry-run=client --validate=false -f "${rendered_manifest}" >/dev/null 2>&1; then
|
||||
cat <<'EOF' >&2
|
||||
kubectl client dry-run failed.
|
||||
For Argo CRDs, this check can still be environment-sensitive and is optional here.
|
||||
Re-run without RUN_KUBECTL_CLIENT_CHECK=1, or use RUN_KUBECTL_SERVER_CHECK=1 against a cluster with the CRDs installed.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_kubectl_server_check() {
|
||||
require_command kubectl
|
||||
kubectl apply --dry-run=server -f "${rendered_manifest}" >/dev/null
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
require_command helm
|
||||
require_command argo
|
||||
|
||||
printf '==> helm lint\n'
|
||||
helm lint "${chart_dir}"
|
||||
|
||||
printf '==> helm template\n'
|
||||
helm template "${release_name}" "${chart_dir}" > "${rendered_manifest}"
|
||||
|
||||
printf '==> argo lint --offline\n'
|
||||
argo lint --offline --kinds=clusterworkflowtemplates "${rendered_manifest}"
|
||||
|
||||
if [[ "${RUN_KUBECTL_CLIENT_CHECK:-0}" == "1" ]]; then
|
||||
printf '==> kubectl apply --dry-run=client\n'
|
||||
run_kubectl_client_check
|
||||
fi
|
||||
|
||||
if [[ "${RUN_KUBECTL_SERVER_CHECK:-0}" == "1" ]]; then
|
||||
printf '==> kubectl apply --dry-run=server\n'
|
||||
run_kubectl_server_check
|
||||
fi
|
||||
@@ -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,19 @@
|
||||
{
|
||||
"name": "tools",
|
||||
"version": "1.0.0",
|
||||
"description": "Custom pipeline utilities for agentguard-ci",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run src",
|
||||
"build": "tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { checkReports } from './enforce-policy.js';
|
||||
|
||||
describe('enforce-policy', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reports-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should find vulnerabilities above threshold in SARIF', () => {
|
||||
const sarifData = {
|
||||
runs: [{
|
||||
results: [
|
||||
{ properties: { 'security-severity': '8.5' } },
|
||||
{ properties: { 'security-severity': '5.0' } }
|
||||
]
|
||||
}]
|
||||
};
|
||||
fs.writeFileSync(path.join(tempDir, 'test.sarif'), JSON.stringify(sarifData));
|
||||
|
||||
const findings = checkReports(tempDir, 7.0);
|
||||
expect(findings).toHaveLength(1);
|
||||
expect(findings[0].name).toBe('test.sarif');
|
||||
expect(findings[0].score).toBe(8.5);
|
||||
});
|
||||
|
||||
it('should find vulnerabilities above threshold in JSON', () => {
|
||||
const jsonData = {
|
||||
findings: [
|
||||
{ cvss: 9.0 },
|
||||
{ score: 6.5 }
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(path.join(tempDir, 'test.json'), JSON.stringify(jsonData));
|
||||
|
||||
const findings = checkReports(tempDir, 7.0);
|
||||
expect(findings).toHaveLength(1);
|
||||
expect(findings[0].name).toBe('test.json');
|
||||
expect(findings[0].score).toBe(9.0);
|
||||
});
|
||||
|
||||
it('should set process.exitCode = 1 for invalid JSON', () => {
|
||||
fs.writeFileSync(path.join(tempDir, 'invalid.json'), '{ "bad": json');
|
||||
|
||||
const findings = checkReports(tempDir, 7.0);
|
||||
expect(findings).toHaveLength(0);
|
||||
expect(process.exitCode).toBe(1);
|
||||
process.exitCode = 0; // reset for other tests
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
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 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.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const reportsDir = '/workspace/reports';
|
||||
let fileNames: string[];
|
||||
|
||||
try {
|
||||
fileNames = (await fs.readdir(reportsDir)).sort();
|
||||
} catch {
|
||||
console.log('No reports directory found.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
const fullPath = path.join(reportsDir, fileName);
|
||||
const stats = await fs.stat(fullPath);
|
||||
if (!stats.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scanType = resolveScanType(fileName);
|
||||
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}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/v2/reimport-scan/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${apiToken}`,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error(`Failed to upload ${fileName}: ${response.status} ${response.statusText} - ${text}`);
|
||||
process.exitCode = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
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().catch((error: unknown) => {
|
||||
console.error('Unexpected upload failure:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node", "vitest/globals"],
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user