Skip to content
1 change: 1 addition & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export {
computeLaneFit,
type GoalModelInput,
} from "./goal-model.js";
export {
classifyContributorFit,
type ContributorFit,
type ContributorFitCheck,
Expand Down
10 changes: 8 additions & 2 deletions packages/gittensory-engine/src/opportunity-freshness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ function pickTimestamp(issue: FreshnessIssue): string | null {
return null;
}

// No usable timestamp survived pickTimestamp's updatedAt->createdAt fallback -- an unknown age must never
// register as "just updated" (age 0, the freshest possible score). Floor it to a large sentinel so
// computeOpportunityFreshness's exponential decay clamps straight to the 0.05 floor, matching how a genuinely
// stale issue scores, not a fresh one.
const UNKNOWN_AGE_DAYS = 9999;

function issueAgeDays(value: string | null, nowMs: number): number {
if (!value) return 0;
if (!value) return UNKNOWN_AGE_DAYS;
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) return 0;
if (!Number.isFinite(parsed)) return UNKNOWN_AGE_DAYS;
return Math.floor((nowMs - parsed) / 86_400_000);
}

Expand Down
21 changes: 5 additions & 16 deletions src/review/safety.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { AdvisoryFinding } from "../types";
import { neutralizePromptInjection, safeReviewTitle } from "./prompt-injection";
import { scanForSecrets } from "./secrets-scan";
import { scanPrDiffForSecretKinds } from "./secrets-scan";

// Concrete credential formats only — NOT the weak heuristics (`seed_or_mnemonic` / `bittensor_key`) that
// false-positive on legitimate config/workflow content. A `coldkey:` / `hotkey =` line or the word
Expand Down Expand Up @@ -98,21 +98,10 @@ export function secretLeakFinding(diff: string): AdvisoryFinding | null {
// secret-shaped string (e.g. deleting/defanging a test fixture, or rotating a credential out). Added/renamed
// file paths are also committed PR state, but buildSecretScanDiff carries them only in `### path (status)`
// headers, so keep those metadata lines while still dropping modified/removed headers and `+++` patch headers.
const added = diff
.split("\n")
.filter(
(line) =>
(line.startsWith("+") && !line.startsWith("+++")) ||
/^### .+ \((?:added|renamed)\) /.test(line),
)
.join("\n");
// Only CONCRETE credential formats hard-block. The raw scanner also returns the weak `seed_or_mnemonic` /
// `bittensor_key` heuristics, which false-positive on `coldkey:` / `hotkey =` / "mnemonic" lines in
// legitimate config/workflow files (RC6); those are filtered out here so they never produce a `secret_leak`
// blocker. A real token (github_token, aws_access_key, …) still blocks regardless of which file it is in.
const kinds = scanForSecrets(added).kinds.filter((kind) =>
HARD_SECRET_KINDS.has(kind),
);
// scanPrDiffForSecretKinds walks the diff line-by-line (with a bounded cross-line literal join on consecutive
// added lines, #2454) instead of joining all `+` lines into one blob — that join would miss a credential
// split across adjacent assignments and would also ignore hunk/context boundaries the gate must respect.
const kinds = scanPrDiffForSecretKinds(diff).filter((kind) => HARD_SECRET_KINDS.has(kind));
if (kinds.length === 0) return null;
return {
code: "secret_leak",
Expand Down
104 changes: 104 additions & 0 deletions src/review/secrets-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,107 @@ export function scanForSecrets(text: string): SecretScanResult {
if (hasGenericSecretAssignment(text)) kinds.push("generic_secret_assignment");
return { found: kinds.length > 0, kinds };
}

/** Extract quoted string-literal inner text from one source line (for cross-line join below). */
function extractStringLiteralContents(line: string): string[] {
const literals: string[] = [];
const re = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
let match: RegExpExecArray | null;
while ((match = re.exec(line)) !== null) literals.push(match[0].slice(1, -1));
return literals;
}

function formatSecretKindsFromText(text: string): string[] {
const kinds = SECRET_PATTERNS.filter((pattern) => pattern.re.test(text)).map((pattern) => pattern.name);
if (hasGenericSecretAssignment(text)) kinds.push("generic_secret_assignment");
return kinds;
}

/** True for unified-diff file headers (`+++ b/path`, `--- a/path`), not added content like `+++const`.
* Mirrors review-enrichment/src/analyzers/diff-lines.ts `isDiffFileHeaderLine`. */
function isUnifiedDiffFileHeaderLine(line: string): boolean {
return /^(?:\+\+\+|---) (?:[ab]\/|\/dev\/null)/.test(line);
}

/** Scan a PR diff for secret kinds introduced on added lines (and added/renamed file headers). Per-line regex
* first; then a bounded cross-line join of consecutive added lines' adjacent string literals (#2454) so a
* credential split across `const a = "AKIA…"; const b = "REST";` still trips the unconditional gate. Context,
* removed, hunk, and file-section boundaries reset both the literal-join window and generic-assignment runs. */
export function scanPrDiffForSecretKinds(diff: string): string[] {
const found = new Set<string>();
let inFileSection = false;
let inHunk = false;
let previousLiterals: string[] = [];
let addedRun: string[] = [];

const resetJoinState = (): void => {
previousLiterals = [];
addedRun = [];
};

const noteGenericFromAddedRun = (): void => {
if (found.has("generic_secret_assignment") || addedRun.length === 0) return;
if (hasGenericSecretAssignment(addedRun.join("\n"))) {
found.add("generic_secret_assignment");
}
};

for (const line of diff.split("\n")) {
if (line === "") {
inFileSection = false;
inHunk = false;
resetJoinState();
continue;
}
if (/^### .+ \(.+\) /.test(line)) {
inFileSection = true;
inHunk = false;
resetJoinState();
if (/^### .+ \((?:added|renamed)\) /.test(line)) {
for (const kind of formatSecretKindsFromText(line)) found.add(kind);
}
continue;
}
if (line.startsWith("@@")) {
inHunk = true;
resetJoinState();
continue;
}
if (line.startsWith("+")) {
if (!inFileSection) continue;
// Skip pre-hunk file headers only; inside a hunk `+++…` is added content, not a header.
if (!inHunk && isUnifiedDiffFileHeaderLine(line)) continue;
const content = line.slice(1);
addedRun.push(content);
let matched = false;
for (const kind of formatSecretKindsFromText(content)) {
found.add(kind);
matched = true;
}
noteGenericFromAddedRun();
const currentLiterals = extractStringLiteralContents(content);
const lastPrevious = previousLiterals.at(-1);
const firstCurrent = currentLiterals[0];
if (!matched && lastPrevious !== undefined && firstCurrent !== undefined) {
const joined = lastPrevious + firstCurrent;
for (const pattern of SECRET_PATTERNS) {
if (pattern.re.test(joined)) {
found.add(pattern.name);
break;
}
}
}
previousLiterals = currentLiterals;
continue;
}
if (line.startsWith("-")) {
resetJoinState();
continue;
}
if (inHunk && line.startsWith(" ")) {
resetJoinState();
}
}

return [...found];
}
13 changes: 13 additions & 0 deletions test/unit/safety-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,17 @@ describe("secretLeakFinding scans only ADDED lines", () => {
const diff = `### fixtures/${fakeToken}.txt (removed) +0/-1\n@@\n-const unrelated = 1;`;
expect(secretLeakFinding(diff)).toBeNull();
});

it("flags a credential split across consecutive added lines (#2454)", () => {
const awsKeyFragmentA = "AKIA" + "IOSFODNN7";
const awsKeyFragmentB = "EXAMPLE";
const diff = [
"### src/config.ts (modified) +2/-0",
"@@ -1,0 +1,2 @@",
`+const part1 = "${awsKeyFragmentA}";`,
`+const part2 = "${awsKeyFragmentB}";`,
].join("\n");
expect(secretLeakFinding(diff)?.code).toBe("secret_leak");
expect(secretLeakFinding(diff)?.title).toContain("aws_access_key");
});
});
Loading
Loading