diff --git a/.github/workflows/performance-regression.yml b/.github/workflows/performance-regression.yml new file mode 100644 index 0000000..f790770 --- /dev/null +++ b/.github/workflows/performance-regression.yml @@ -0,0 +1,32 @@ +name: Performance Regression Detection + +on: + pull_request: + branches: [ main ] + +jobs: + performance-test: + runs-on: ubuntu-latest + services: + # If you need DBs (Postgres/Redis), define them here + postgres: + image: postgres:15 + ports: ['5432:5432'] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Environment + # Add steps to build your backend (npm install, go build, etc.) + run: | + npm install + npm run build + npm run start & + sleep 10 # Wait for boot + + - name: Run k6 Performance Test + uses: grafana/k6-action@v0.3.0 + with: + filename: tests/performance/critical-paths.js + flags: --env API_URL=http://localhost:3000 diff --git a/src/audit-guard/src/policy-engine.test.ts b/src/audit-guard/src/policy-engine.test.ts index 6e8aa9e..009cb66 100644 --- a/src/audit-guard/src/policy-engine.test.ts +++ b/src/audit-guard/src/policy-engine.test.ts @@ -452,6 +452,17 @@ describe("PolicyEngine", () => { }); }); + it("should return NON_COMPLIANT when bundle tampering is detected", async () => { + // Mock the verifier to return a BundleTampered error + // Mock the verifier.verify() to return { verified: false, errors: [{ code: 'BundleTampered', ... }] } + + const result = await engine.evaluate(mockPrData); + + expect(result.status).toBe("NON_COMPLIANT"); + expect(result.violations_count).toBeGreaterThan(0); + expect(result.summary).toContain("tampering detected"); +}); + describe("Large Changes", () => { it("should warn about many files modified", async () => { const files = Array.from({ length: 25 }, (_, i) => `src/file${i}.ts`); diff --git a/src/audit-guard/src/policy-engine.ts b/src/audit-guard/src/policy-engine.ts index 701a800..454bd94 100644 --- a/src/audit-guard/src/policy-engine.ts +++ b/src/audit-guard/src/policy-engine.ts @@ -195,19 +195,17 @@ export class PolicyEngine { } } - /** + /** * Second, independent control against a compromised CI runner tampering with - * the Rego policy bundle (Issue #171 / VAG-003). When bundle signing is - * configured (POLICY_BUNDLE_SIGNATURE + POLICY_BUNDLE_SIGNERS), verify the - * on-disk bundle against a manifest signed offline by a trusted maintainer - * key. Observational-only: failures are surfaced as high/critical WARNINGS - * (which reportToDashboard() forwards as alerts) — they never silently drop, - * but this control holds no on-chain halt authority. + * the Rego policy bundle (Issue #171 / VAG-003). + * + * UPDATED (Issue #311): Confirmed tampering now promotes status to + * NON_COMPLIANT to block the build. */ private applyPolicyBundleVerification(result: EvaluationResult): void { const verifier = new PolicyBundleVerifier({ policiesDir: this.policiesDir }); if (!verifier.isConfigured()) { - return; // Enforcement not enabled for this repo/environment. + return; } const verification = verifier.verify(); @@ -215,25 +213,46 @@ export class PolicyEngine { return; } + let hasTampering = false; + for (const err of verification.errors) { - const critical = + const isTampered = err.code === PolicyBundleErrorCode.BundleTampered || err.code === PolicyBundleErrorCode.SignatureInvalid || err.code === PolicyBundleErrorCode.UntrustedSigner; - result.warnings.push({ - rule: err.code, - severity: critical ? "CRITICAL" : "HIGH", - message: `⚠️ Policy bundle signature verification failed: ${err.message}`, - detail: err.detail || err.message, - }); + + if (isTampered) { + hasTampering = true; + // Promote to violations to block build + result.violations.push({ + rule: err.code, + severity: "CRITICAL", + message: `❌ CRITICAL: Policy bundle integrity check failed: ${err.message}`, + detail: err.detail || err.message, + }); + } else { + // Keep non-critical verification issues as warnings + result.warnings.push({ + rule: err.code, + severity: "HIGH", + message: `⚠️ Policy bundle signature verification warning: ${err.message}`, + detail: err.detail || err.message, + }); + } } + // Update counts + result.violations_count = result.violations.length; result.warnings_count = result.warnings.length; - if (result.status === "COMPLIANT") { + + // Determine final status + if (hasTampering) { + result.status = "NON_COMPLIANT"; + result.summary = "❌ Policy bundle tampering detected — blocking build"; + } else if (result.warnings.length > 0 && result.status === "COMPLIANT") { result.status = "WARNING"; } } - /** * Evaluate using OPA CLI */ diff --git a/tests/performance/critical-paths.js b/tests/performance/critical-paths.js new file mode 100644 index 0000000..d56cbf0 --- /dev/null +++ b/tests/performance/critical-paths.js @@ -0,0 +1,30 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + thresholds: { + // CRITICAL: Fail if 99% of requests take longer than 100ms + http_req_duration: ['p(99)<100'], + // Ensure availability target + http_req_failed: ['rate<0.01'], + }, + stages: [ + { duration: '1m', target: 50 }, // Ramp up to 50 users + { duration: '3m', target: 50 }, // Stay at 50 users + { duration: '1m', target: 0 }, // Ramp down + ], +}; + +export default function () { + const params = { + headers: { 'Content-Type': 'application/json' }, + }; + + const responses = http.batch([ + ['GET', `${__ENV.API_URL}/v1/status`, null, params], + ['GET', `${__ENV.API_URL}/v1/node/verify`, null, params], + ]); + + check(responses[0], { 'status is 200': (r) => r.status === 200 }); + sleep(1); +}