diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index ae721a4..6f473ac 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -16,12 +16,13 @@ */ import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; const ROOT = resolve(import.meta.dirname, ".."); const TEMP_DIR = join(ROOT, ".test-temp", "package-smoke"); const FIXTURES_DIR = join(ROOT, "fixtures", "demo-shop-ts"); +const DIFF_FIXTURE_DIR = join(TEMP_DIR, "diff-fixture"); const DIST_DIR = join(ROOT, "dist"); const NPM_CACHE_DIR = join(ROOT, ".qh", "npm-cache"); const NPM_ENV = { ...process.env, npm_config_cache: NPM_CACHE_DIR }; @@ -41,6 +42,27 @@ function removeDirectory(target) { rmSync(target, REMOVE_DIRECTORY_OPTIONS); } +function runGit(args, cwd) { + execFileSync("git", args, { cwd, stdio: "ignore" }); +} + +function prepareDiffFixture() { + removeDirectory(DIFF_FIXTURE_DIR); + cpSync(FIXTURES_DIR, DIFF_FIXTURE_DIR, { recursive: true }); + removeDirectory(join(DIFF_FIXTURE_DIR, ".git")); + + writeFileSync(join(DIFF_FIXTURE_DIR, "package-smoke-baseline.txt"), "baseline\n"); + runGit(["init", "--quiet"], DIFF_FIXTURE_DIR); + runGit(["config", "user.name", "code-to-gate package smoke"], DIFF_FIXTURE_DIR); + runGit(["config", "user.email", "package-smoke@code-to-gate.invalid"], DIFF_FIXTURE_DIR); + runGit(["add", "-f", "."], DIFF_FIXTURE_DIR); + runGit(["-c", "commit.gpgSign=false", "commit", "--quiet", "-m", "Create baseline fixture"], DIFF_FIXTURE_DIR); + + writeFileSync(join(DIFF_FIXTURE_DIR, "package-smoke-change.ts"), "export const packageSmokeChange = true;\n"); + runGit(["add", "-f", "package-smoke-change.ts"], DIFF_FIXTURE_DIR); + runGit(["-c", "commit.gpgSign=false", "commit", "--quiet", "-m", "Add diff fixture change"], DIFF_FIXTURE_DIR); +} + function cleanup() { // Always cleanup tarball and temp directory if (tgzPath && existsSync(tgzPath)) { @@ -207,9 +229,10 @@ try { removeDirectory(diffOutDir); mkdirSync(diffOutDir, { recursive: true }); - // Use git refs from demo-shop-ts (HEAD vs HEAD~1) - const diffResult = execFileSync(process.execPath, [cliPath, "diff", FIXTURES_DIR, "--base", "HEAD~1", "--head", "HEAD", "--out", diffOutDir], { - cwd: FIXTURES_DIR, // Run inside fixture directory for git context + // Build an isolated two-commit repository so the test does not depend on checkout depth. + prepareDiffFixture(); + const diffResult = execFileSync(process.execPath, [cliPath, "diff", DIFF_FIXTURE_DIR, "--base", "HEAD~1", "--head", "HEAD", "--out", diffOutDir], { + cwd: DIFF_FIXTURE_DIR, encoding: "utf8", timeout: 60000, }); // Verify diff-analysis.json exists diff --git a/src/rules/__tests__/deprecated-api.test.ts b/src/rules/__tests__/deprecated-api.test.ts index 4521e36..97bdd77 100644 --- a/src/rules/__tests__/deprecated-api.test.ts +++ b/src/rules/__tests__/deprecated-api.test.ts @@ -96,6 +96,17 @@ function createBuffer(str) { expect(findings[0].title).toContain("new Buffer()"); }); + it("does not treat arrayBuffer() as the deprecated global Buffer()", () => { + const context = createMockContext([ + { + path: "src/response.ts", + content: "const raw = new Uint8Array(await response.arrayBuffer());", + }, + ]); + + expect(DEPRECATED_API_USAGE_RULE.evaluate(context)).toHaveLength(0); + }); + it("should detect fs.exists", () => { const context = createMockContext([ { diff --git a/src/rules/__tests__/hardcoded-secret.test.ts b/src/rules/__tests__/hardcoded-secret.test.ts index 987a037..a77d27f 100644 --- a/src/rules/__tests__/hardcoded-secret.test.ts +++ b/src/rules/__tests__/hardcoded-secret.test.ts @@ -67,6 +67,16 @@ describe("HARDCODED_SECRET_RULE", () => { expect(findings).toHaveLength(0); }); + it("ignores unrelated assignments on lines that only mention sensitive selectors or fixture IDs", () => { + const content = [ + 'await page.addStyleTag({ content: \'input[type="password"], input[name*="token"] { color: transparent }\' });', + 'const run = configure({ actionCatalog: [{ id: "secret-candidate-value", inputProfileId: "protected-profile" }] });', + ].join("\n"); + const findings = HARDCODED_SECRET_RULE.evaluate(createContext("src/runtime.ts", content)); + + expect(findings).toHaveLength(0); + }); + it("ignores the rule implementation itself", () => { const content = [ 'const SECRET_VAR_NAMES = ["password", "api_key"];', diff --git a/src/rules/deprecated-api.ts b/src/rules/deprecated-api.ts index 5eee952..ccef93f 100644 --- a/src/rules/deprecated-api.ts +++ b/src/rules/deprecated-api.ts @@ -43,7 +43,7 @@ const DEPRECATED_NODE_APIS = [ // Buffer { pattern: /new\s+Buffer\s*\(/g, name: "new Buffer()", replacement: "Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()" }, - { pattern: /Buffer\(\s*\)/g, name: "Buffer()", replacement: "Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()" }, + { pattern: /(? { + const candidate = value.replace(/[^a-z0-9]/g, ""); + return normalized === candidate || normalized.startsWith(candidate) || normalized.endsWith(candidate); + }); +} + function isSafeValue(value: string): boolean { const safeValues = ["changeme", "your_key_here", "replace_me", "xxx", "test", "example"]; return safeValues.some(s => value.toLowerCase().includes(s)) || @@ -105,24 +113,22 @@ export const HARDCODED_SECRET_RULE: RulePlugin = { } } - // Check for secret variable assignments - if (SECRET_VAR_NAMES.some(v => line.toLowerCase().includes(v))) { - const match = line.match(/([A-Za-z_][A-Za-z0-9_]*)\s*[=:]\s*["']([^"']{16,})["']/); - if (match && !isSafeValue(match[2])) { - if (isDescriptiveMetadataAssignment(match[1])) continue; - - const excerpt = line.trim(); - findings.push({ - id: generateFindingId("HARDCODED_SECRET_VAR", file.path, lineNum + 1), - ruleId: "HARDCODED_SECRET", - severity: "high", - confidence: 0.7, - title: `Possible secret in variable: ${match[1]}`, - summary: `Variable "${match[1]}" may contain hardcoded secret`, - evidence: [createEvidence(file.path, lineNum + 1, lineNum + 1, "text", excerpt)], - category: "security", - }); - } + // Check only assignments whose own variable name denotes a secret. + for (const match of line.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*[=:]\s*["']([^"']{16,})["']/g)) { + if (!isSecretVariableName(match[1]) || isSafeValue(match[2]) || isDescriptiveMetadataAssignment(match[1])) continue; + + const excerpt = line.trim(); + findings.push({ + id: generateFindingId("HARDCODED_SECRET_VAR", file.path, lineNum + 1), + ruleId: "HARDCODED_SECRET", + severity: "high", + confidence: 0.7, + title: `Possible secret in variable: ${match[1]}`, + summary: `Variable "${match[1]}" may contain hardcoded secret`, + evidence: [createEvidence(file.path, lineNum + 1, lineNum + 1, "text", excerpt)], + category: "security", + }); + break; } } }