Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/rules/__tests__/deprecated-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down
10 changes: 10 additions & 0 deletions src/rules/__tests__/hardcoded-secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];',
Expand Down
2 changes: 1 addition & 1 deletion src/rules/deprecated-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
{ pattern: /util\.isUndefined\s*\(/g, name: "util.isUndefined", replacement: "value === undefined" },

// fs module
{ pattern: /fs\.exists\s*\(/g, name: "fs.exists", replacement: "fs.existsSync() or fs.stat()" },

Check warning on line 33 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

DEPRECATED_API_USAGE [medium]

The API 'fs.existsSync' is deprecated. Recommended replacement: fs.statSync() or fs.accessSync(). Using deprecated APIs creates maintenance debt and may lead to breaking changes.
{ pattern: /fs\.existsSync\s*\(/g, name: "fs.existsSync", replacement: "fs.statSync() or fs.accessSync()" },

// crypto module
Expand All @@ -42,11 +42,11 @@
{ pattern: /domain\.run\s*\(/g, name: "domain.run", replacement: "async_hooks or proper error handling" },

// Buffer
{ pattern: /new\s+Buffer\s*\(/g, name: "new Buffer()", replacement: "Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()" },

Check warning on line 45 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

DEPRECATED_API_USAGE [medium]

The API 'new Buffer()' is deprecated. Recommended replacement: Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe(). Using deprecated APIs creates maintenance debt and may lead to breaking changes.
{ pattern: /Buffer\(\s*\)/g, name: "Buffer()", replacement: "Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()" },
{ pattern: /(?<![\w$.])Buffer\(\s*\)/g, name: "Buffer()", replacement: "Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()" },

Check warning on line 46 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

DEPRECATED_API_USAGE [medium]

The API 'Buffer()' is deprecated. Recommended replacement: Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe(). Using deprecated APIs creates maintenance debt and may lead to breaking changes.

// path module
{ pattern: /path\.exists\s*\(/g, name: "path.exists", replacement: "fs.existsSync() or fs.accessSync()" },

Check warning on line 49 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

DEPRECATED_API_USAGE [medium]

The API 'fs.existsSync' is deprecated. Recommended replacement: fs.statSync() or fs.accessSync(). Using deprecated APIs creates maintenance debt and may lead to breaking changes.
];

const DEPRECATED_BROWSER_APIS = [
Expand Down Expand Up @@ -130,14 +130,14 @@
const line = lines[i];
const lineNum = i + 1;

// Check for SMELL comment markers
if (line.includes("SMELL: DEPRECATED_API_USAGE") || line.includes("SMELL - Lines")) {
inSmellComment = true;
smellStartLine = lineNum;
continue;

Check warning on line 137 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

CLIENT_TRUSTED_PRICE [critical]

Code explicitly marked as vulnerable: client-controlled price/total value is used directly. This allows price manipulation attacks.
}

// Check for END SMELL marker

Check warning on line 140 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

CLIENT_TRUSTED_PRICE [critical]

Code explicitly marked as vulnerable: client-controlled price/total value is used directly. This allows price manipulation attacks.

Check warning on line 140 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

UNSAFE_REDIRECT [high]

Redirect operation using user-supplied URL without validation at line 134-140. This could enable open redirect attacks.

Check warning on line 140 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

DEPRECATED_API_USAGE [medium]

Deprecated API used at lines 134-140. Deprecated APIs may be removed in future versions and can cause breaking changes.

Check warning on line 140 in src/rules/deprecated-api.ts

View workflow job for this annotation

GitHub Actions / code-to-gate Analysis

MISSING_INPUT_SANITIZATION [critical]

User input used without sanitization at lines 134-140. Injection attacks (XSS, SQL injection, command injection) are top OWASP vulnerabilities.
if (inSmellComment && line.includes("END SMELL")) {
findings.push({
id: generateFindingId("DEPRECATED_API_USAGE", file.path, smellStartLine),
Expand Down
42 changes: 24 additions & 18 deletions src/rules/hardcoded-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ function isDescriptiveMetadataAssignment(name: string): boolean {
return ["description", "summary", "title", "useCase", "recommendedAction", "narrative"].includes(name);
}

function isSecretVariableName(name: string): boolean {
const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, "");
return SECRET_VAR_NAMES.some(value => {
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)) ||
Expand Down Expand Up @@ -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;
}
}
}
Expand Down