From 0c18264403277e24ded6c7d242db3b1319e2dc05 Mon Sep 17 00:00:00 2001 From: agentguard-dev Date: Fri, 11 Sep 2026 22:53:09 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20AWS-Doku-Beispielkeys=20l=C3=B6sen=20SEC?= =?UTF-8?q?RET-001=20nicht=20mehr=20aus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strukturelle Lösung statt Literal-Liste: Access-Key-IDs auf '...EXAMPLE' werden vor dem Matching neutralisiert. Eine Liste konkreter Beispiel-Keys hätte selbst wie ein Secret ausgesehen und die veröffentlichte v0-Action dazu gebracht, diese Datei zu flaggen (CI-Smoke-Test rot). --- src/rules.js | 25 ++++++++++++++++++++++--- test/scanner.test.js | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/rules.js b/src/rules.js index e9f49a7..9789314 100644 --- a/src/rules.js +++ b/src/rules.js @@ -219,12 +219,31 @@ export const HOOK_001 = { }; // Rule 5 — API keys/secrets committed (outside *.example templates). +// AWS-Dokumentations-Beispiele (Access-Key-IDs auf "…EXAMPLE") sind keine +// committeten Credentials: Agent-Configs dokumentieren häufig Cloud-Setup +// (z. B. ein AGENTS.md für einen AWS-Emulator). +// +// Bewusst KEINE Liste konkreter Beispiel-Keys: solche Literale sehen selbst wie +// ein Secret aus und triggern ältere Scanner-Versionen (im CI-Smoke-Test passiert: +// die veröffentlichte Auto-Action flaggte diese Datei). Stattdessen strukturell: +// echte Access-Key-IDs enden praktisch nie auf "EXAMPLE" (36^7 ≈ 7,8e10 Fälle). +const AWS_DOC_EXAMPLE_SUFFIX = "EXAMPLE"; + +function stripAwsDocExamples(content) { + return content.replace(/AKIA[0-9A-Z]{16}/g, (m) => + m.endsWith(AWS_DOC_EXAMPLE_SUFFIX) ? "" : m + ); +} + export const SECRET_001 = { id: "SECRET-001", severity: "critical", description: "High-entropy credential pattern committed to the repository.", scan({ rel, content }) { if (/(\.example|\.sample|\.template|\.bak|\.orig|\.fixture)$/i.test(rel)) return null; + // Dokumentations-Beispiele neutralisieren, BEVOR gematcht wird. Das + // Entfernen berührt keine Zeilenumbrüche, Zeilennummern bleiben korrekt. + const hay = stripAwsDocExamples(content); const patterns = { "sk-…": /sk-[A-Za-z0-9]{20,}/g, "ghp_…": /ghp_[A-Za-z0-9]{25,}/g, @@ -241,11 +260,11 @@ export const SECRET_001 = { let firstLine = null; for (const [kind, re] of Object.entries(patterns)) { re.lastIndex = 0; - if (re.test(content)) { + if (re.test(hay)) { detected.push(kind); if (firstLine === null) { - const idx = content.search(re); - firstLine = idx >= 0 ? lineOf(content, idx) : null; + const idx = hay.search(re); + firstLine = idx >= 0 ? lineOf(hay, idx) : null; } } } diff --git a/test/scanner.test.js b/test/scanner.test.js index 97d0675..aa27603 100644 --- a/test/scanner.test.js +++ b/test/scanner.test.js @@ -196,6 +196,28 @@ test("real slack token format is a secret", async () => { assert.equal(res.severity, "critical"); }); +test("AWS documentation example key is not a secret", async () => { + const { SECRET_001 } = await import("../src/rules.js"); + // Bewusst zusammengesetzt (wie beim Slack-Token): das Literal würde ältere + // Scanner-Versionen triggern. Zur Laufzeit ist es exakt der Beispiel-Key aus + // den AWS-Docs (Form AKIA…EXAMPLE). + const AWS_DOC_EXAMPLE = "AKIA" + "IOSFODNN7EXAMPLE"; + const res = SECRET_001.scan({ + rel: "AGENTS.md", + content: + 'aws_access_key_id="' + AWS_DOC_EXAMPLE + '", aws_secret_access_key="test", region="us-east-1"\n', + }); + assert.equal(res, null, "AWS docs example credentials document setup, they are not leaks"); +}); + +test("non-example AWS key shape is still flagged as secret", async () => { + const { SECRET_001 } = await import("../src/rules.js"); + const key = "AKIA" + "3J7Q2PLZKQ9WRTUV"; + const res = SECRET_001.scan({ rel: "AGENTS.md", content: "aws_access_key_id=" + key + "\n" }); + assert.ok(res, "a non-example AWS key shape must stay critical"); + assert.equal(res.severity, "critical"); +}); + test("everyday prose with curl is not instruction override", async () => { const { INSTR_OVR_001 } = await import("../src/rules.js"); const res = INSTR_OVR_001.scan({