/** * pregate/allowlist — the policy's `allowlist` applied to the secrets check's * findings (Lane D, GSTA-112; harness-evals #81/#83, GSTA-102, GSTA-106). An * entry names a gstack-redact finding id or a non-secret category, and scopes * it by path globs and/or a regular expression matched at the finding's * position in the added line (anchored there, so a dummy token's shape never * covers a real key that happens to share its line). `env.value`, the real * value of a credential-named environment variable, is never consulted * against the list: validation refuses an entry naming it, and this matcher * keeps such a finding whatever the entries say (#3449). */ import { matchesAny } from '../ship-policy'; import { sha256 } from './context'; import { ALLOWLISTABLE_CATEGORIES, nonAllowlistable, type AllowlistEntry } from './config'; export interface AllowlistFinding { id: string; category: string; severity: string; file: string; line?: number; col?: number; /** The added line the finding sits on (for `pattern`); absent when the scanner gave no position. */ lineText?: string; } export interface Allowlisted { finding: F; index: number; entry: AllowlistEntry } /** Whether `entry` names this finding: its id, or its category when the category may be named whole. */ function names(entry: AllowlistEntry, f: AllowlistFinding): boolean { if (entry.finding === f.id) return true; return (ALLOWLISTABLE_CATEGORIES as readonly string[]).includes(entry.finding) && entry.finding === f.category; } /** Whether a finding has the position a `pattern` needs: the added line and a 1-based column. */ const positioned = (f: AllowlistFinding) => f.lineText !== undefined && !!f.col; /** The pattern, anchored at the finding's column in its added line and ending at a token boundary (so a short pattern never covers the head of a longer value); a finding with no position never matches a pattern. */ function patternHits(entry: AllowlistEntry, f: AllowlistFinding): boolean { if (entry.pattern === undefined) return true; if (!positioned(f)) return false; let re: RegExp; try { re = new RegExp(`^(?:${entry.pattern})(?![\\w-])`); } catch { return false; } return re.test(f.lineText!.slice(f.col! - 1)); } /** * Split findings into the ones an entry covers (with the entry) and the ones * kept; env.value and engine.* are always kept. `untested` lists kept findings * that a pattern entry named by id and path but could not test because the * scanner gave no position, so the table says why the entry was inert. */ export function applyAllowlist(findings: readonly F[], entries: readonly AllowlistEntry[]): { kept: F[]; allowlisted: Allowlisted[]; untested: Allowlisted[] } { const kept: F[] = []; const allowlisted: Allowlisted[] = []; const untested: Allowlisted[] = []; for (const f of findings) { const scoped = (e: AllowlistEntry) => names(e, f) && (!e.paths || matchesAny(e.paths, f.file)); const index = nonAllowlistable(f.id) ? -1 : entries.findIndex(e => scoped(e) && patternHits(e, f)); if (index >= 0) { allowlisted.push({ finding: f, index, entry: entries[index]! }); continue; } kept.push(f); if (nonAllowlistable(f.id) || positioned(f)) continue; const named = entries.findIndex(e => e.pattern !== undefined && scoped(e)); if (named >= 0) untested.push({ finding: f, index: named, entry: entries[named]! }); } return { kept, allowlisted, untested }; } /** sha256 of the entries, for the check's `inputs.allowlist`. */ export const allowlistDigest = (entries: readonly AllowlistEntry[]) => sha256(JSON.stringify(entries)); /** One table line per finding a pattern entry named but could not test: the finding is kept. */ export function describeUntested(a: Allowlisted): string { return `allowlist[${a.index}] ${a.entry.finding} not tested on ${a.finding.id} (${a.finding.file}): the scanner gave no position for its pattern; the finding is kept`; } /** One table line per allowlisted finding: what, where, and which entry (with its reason) covered it. */ export function describeAllowlisted(a: Allowlisted): string { const where = a.finding.line ? ` (${a.finding.file}, added line ${a.finding.line})` : ` (${a.finding.file})`; return `allowlisted ${a.finding.id}${where} by allowlist[${a.index}] ${a.entry.finding}: ${a.entry.reason}`; }