or rule engine, severity thresholds, and evidence requirements.
-
Orchestrate Execution in CI/CD
The orchestrator runs as a pipeline step, invoking scanners asynchronously, normalizing output formats, and applying policy rules. Results are aggregated before the pipeline proceeds.
-
Collect and Cryptographically Sign Evidence
Raw scan outputs, policy versions, and execution metadata are bundled into a JSON report. The report is signed using a pipeline-managed key to establish chain of custody.
-
Route Findings to Triage and Compliance Stores
High-severity findings block merges. Medium/low findings are logged to a centralized compliance database. Auditors query the signed evidence store directly.
TypeScript Audit Orchestrator
import { execSync } from 'child_process';
import { createHash, sign } from 'crypto';
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
interface AuditPolicy {
id: string;
name: string;
target: 'sast' | 'sca' | 'iac' | 'container';
severityThreshold: 'critical' | 'high' | 'medium' | 'low';
scanner: string;
evidencePath: string;
}
interface AuditResult {
policyId: string;
status: 'pass' | 'fail' | 'error';
findings: number;
artifacts: string[];
timestamp: string;
signature?: string;
}
export class SecurityAuditOrchestrator {
private policies: AuditPolicy[];
private results: AuditResult[] = [];
constructor(policies: AuditPolicy[]) {
this.policies = policies;
}
async execute(): Promise<AuditResult[]> {
for (const policy of this.policies) {
try {
const output = execSync(policy.scanner, { encoding: 'utf-8', timeout: 300000 });
const findings = this.parseFindings(output, policy.severityThreshold);
const result: AuditResult = {
policyId: policy.id,
status: findings > 0 ? 'fail' : 'pass',
findings,
artifacts: [policy.evidencePath],
timestamp: new Date().toISOString()
};
mkdirSync(policy.evidencePath, { recursive: true });
writeFileSync(join(policy.evidencePath, `${policy.id}.json`), JSON.stringify(result, null, 2));
this.results.push(result);
} catch (err) {
this.results.push({
policyId: policy.id,
status: 'error',
findings: 0,
artifacts: [],
timestamp: new Date().toISOString()
});
}
}
return this.results;
}
private parseFindings(raw: string, threshold: string): number {
// Normalize scanner output to count findings >= threshold
const lines = raw.split('\n');
let count = 0;
for (const line of lines) {
if (line.includes(threshold) || line.includes('CRITICAL') || line.includes('HIGH')) {
count++;
}
}
return count;
}
signReport(privateKeyPem: string): string {
const payload = JSON.stringify(this.results);
const hash = createHash('sha256').update(payload).digest('hex');
const signature = sign('sha256', Buffer.from(hash), privateKeyPem, 'hex');
return signature;
}
}
// Usage example
const policies: AuditPolicy[] = [
{
id: 'SAST-001',
name: 'Static Application Security Test',
target: 'sast',
severityThreshold: 'high',
scanner: 'npx eslint --format json src/ 2>/dev/null || true',
evidencePath: './audit-evidence/sast'
},
{
id: 'SCA-002',
name: 'Software Composition Analysis',
target: 'sca',
severityThreshold: 'critical',
scanner: 'npx audit-ci --critical',
evidencePath: './audit-evidence/sca'
}
];
const orchestrator = new SecurityAuditOrchestrator(policies);
orchestrator.execute().then(res => console.log('Audit complete:', res.length, 'policies evaluated'));
Architecture Decisions and Rationale
Policy-as-Code over GUI Configuration
Declarative policies stored in Git enable version control, peer review, and rollback. GUI-based audit tools create configuration drift and make compliance evidence difficult to reproduce.
Async Execution with Normalized Output
Scanners produce inconsistent JSON/XML formats. The orchestrator abstracts scanner output into a unified AuditResult interface. This allows swapping scanners (Trivy, Snyk, Semgrep, Checkov) without rewriting pipeline logic.
Cryptographic Evidence Signing
Compliance auditors require tamper-evident records. Signing the aggregated report with a pipeline-managed private key establishes chain of custody. The signature verifies that evidence was generated at a specific timestamp and has not been altered post-execution.
Decoupled Evidence Storage
Raw scan outputs are preserved alongside normalized results. This satisfies auditors who require original artifacts while enabling engineers to query structured data for remediation tracking.
Pitfall Guide
1. Treating Automation as a Silver Bullet
Automating scans does not eliminate context. A critical vulnerability in a production API requires different handling than the same finding in a deprecated test module. Automation must include environment tagging, asset criticality scoring, and risk-based routing. Otherwise, teams drown in noise and disable the pipeline.
Best Practice: Attach metadata to every policy execution: environment, service owner, data classification, and blast radius. Use this metadata to filter and prioritize findings before they reach engineers.
2. Over-Scanning Without Triage Logic
Running every scanner on every commit creates alert fatigue. Teams disable security gates after repeated false positives. Automation without signal filtering destroys trust.
Best Practice: Implement a triage engine that deduplicates findings, correlates them with existing tickets, and suppresses known-acceptable risks. Only surface new or escalating findings to developers.
3. Hardcoding Secrets in Audit Configurations
Audit orchestrators often require API tokens for SaaS scanners or cloud credentials for IaC validation. Committing these to repositories violates the very policies being enforced.
Best Practice: Use pipeline secret managers (GitHub Secrets, GitLab CI Variables, HashiCorp Vault). Rotate credentials automatically. Never log or serialize secrets in audit reports.
4. Ignoring Baseline Drift
Security posture changes when dependencies update, cloud configurations shift, or code patterns evolve. Static policies become stale. An audit that passes today may fail tomorrow due to untracked drift.
Best Practice: Maintain a baseline snapshot of compliant state. Compare each execution against the baseline and flag deviations. Version policies alongside infrastructure and application code.
5. Skipping Human-in-the-Loop for Critical Findings
Automation should block merges for critical vulnerabilities, but not all critical findings require immediate rework. Some are false positives, some are mitigated by runtime controls, some are acceptable risks.
Best Practice: Route critical findings to a security triage queue with SLA tracking. Allow approved exceptions with documented risk acceptance and expiration dates. Never auto-approve without audit trail.
6. Poor Evidence Chain of Custody
Auditors reject reports that can be modified post-generation. Storing evidence in mutable storage or skipping cryptographic validation fails compliance reviews.
Best Practice: Write evidence to immutable storage (S3 Object Lock, GCP Bucket Lock, Azure Immutable Blob). Sign every report. Log access attempts. Maintain a separate audit log of policy changes.
7. Failing to Version Audit Policies
When policies change without versioning, historical compliance claims become unverifiable. Auditors cannot confirm whether a finding violated the policy active at the time of deployment.
Best Practice: Tag every policy set with a semantic version. Embed the policy version in each audit report. Store historical policy snapshots alongside evidence.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Early-stage startup (<50 engineers) | CI/CD integrated scanning with basic policy-as-code | Low overhead, fast feedback, covers 80% of compliance needs | Low ($200-$800/mo in tooling) |
| Mid-size product team (50-200 engineers) | Policy-as-code automation + centralized evidence store | Scalable triage, audit-ready evidence, reduces compliance labor | Medium ($1,500-$3,000/mo) |
| Regulated enterprise (finance, healthcare) | Full policy engine + immutable evidence + human triage SLAs | Meets SOC2/ISO/HIPAA continuous monitoring requirements | High ($5,000-$12,000/mo) |
| Open-source maintainer | Lightweight SAST/SCA gates + public audit reports | Transparency builds trust, automates dependency hygiene | Low ($0-$300/mo) |
Configuration Template
# audit-policies.yaml
version: "2.1"
metadata:
org: "acme-corp"
compliance_frameworks: ["SOC2", "ISO27001"]
evidence_retention_days: 365
policies:
- id: "SAST-001"
name: "TypeScript Static Analysis"
target: "sast"
severity_threshold: "high"
scanner: "npx eslint --format json src/ 2>/dev/null || true"
evidence_path: "./audit-evidence/sast"
block_on_fail: true
- id: "SCA-002"
name: "Dependency Vulnerability Check"
target: "sca"
severity_threshold: "critical"
scanner: "npx audit-ci --critical --fail-on-any"
evidence_path: "./audit-evidence/sca"
block_on_fail: true
- id: "IAC-003"
name: "Terraform Security Validation"
target: "iac"
severity_threshold: "high"
scanner: "checkov -d infra/ --framework terraform --compact"
evidence_path: "./audit-evidence/iac"
block_on_fail: true
evidence:
storage: "s3://acme-audit-evidence"
immutability: "object-lock"
signing:
algorithm: "SHA256withRSA"
key_rotation_days: 90
Quick Start Guide
- Install dependencies:
npm i -D typescript @types/node eslint audit-ci checkov
- Create policy file: Save the configuration template above as
audit-policies.yaml in your repository root.
- Run orchestrator: Execute
npx ts-node audit-orchestrator.ts in your CI pipeline step before merge.
- Verify evidence: Check
./audit-evidence/ for signed JSON reports. Upload to immutable storage and attach to your compliance dashboard.
Automation transforms security audits from retrospective compliance exercises into continuous engineering controls. Implement the policy layer, enforce execution gates, and preserve cryptographic evidence. The pipeline becomes the auditor.