/** * content-scan.ts — scans skill BODIES (not just package.json/README) for content * that must never ship in the public @hasna/skills package. * * Five finding categories: * - secret-value: credential-shaped values (API keys, tokens, private keys) * - pii-contact: personal contact PII (E.164 phone numbers, consumer-provider * email addresses) * - pii-personal: personal data ABOUT an identified individual — a real human * name paired with employment/HR/leave/health status, or a * government identifier carrying a real-looking value * - private-context: internal/operational context that leaks the private fleet * (fleet hostnames, home-directory paths with a real user, * internal CLI invocations, internal infra/product names) * - committed-output: operational tool output committed into the package — a * run artifact (timestamped filename, data file under an * exports/ directory) rather than an authored fixture * * The first four categories are matched against file CONTENT (see {@link scanText}). * `committed-output` is matched against the package-relative PATH instead (see * {@link scanPaths}), because what makes an artifact wrong is where it came from, * not what is inside it — a scraped catalog and a hand-written fixture can be * byte-identical in shape. * * All output is REDACTED: secret values are never emitted, phone numbers, emails and * personal names are masked, and only the rule id + a safe redacted marker are * reported. Findings are surfaced in (potentially public) CI logs, so a rule must * never echo the personal data it just found. */ export type ScanCategory = "secret-value" | "pii-contact" | "pii-personal" | "private-context" | "committed-output"; export interface ScanRule { category: ScanCategory; /** Stable, human-readable rule id (safe to print — never a secret value). */ id: string; description: string; pattern: RegExp; /** Optional predicate to drop known-safe example matches (e.g. 555 phone numbers). */ isExample?: (match: string) => boolean; /** * Match against the whole text rather than line by line. Needed for record * shapes whose fields sit on DIFFERENT lines — a YAML export writes the * person on one line and the status on the next, and a per-line regex can * never see that pairing. */ multiline?: boolean; } export interface ScanFinding { file: string; line: number; column: number; category: ScanCategory; ruleId: string; /** Redacted marker — never contains a raw secret value. */ redacted: string; } export declare const SCAN_RULES: ScanRule[]; /** A rule matched against a package-relative path rather than file content. */ export interface ScanPathRule { category: ScanCategory; id: string; description: string; pattern: RegExp; /** Optional predicate to drop known-safe paths (e.g. source trees). */ isExample?: (path: string) => boolean; } /** * Scan package-relative PATHS for committed tool output. Unlike the content * scanners this reads nothing from disk — the path itself is the evidence. The * reported marker is the path verbatim: it is not sensitive, and the maintainer * needs it in order to delete the file. */ export declare function scanPaths(paths: string[]): ScanFinding[]; /** * Produce a redacted marker for a matched value. Secret values are NEVER emitted; * PII (phone numbers, emails, personal names and identifiers) keeps only a short * two-character locating prefix; private-context matches (hostnames, paths, CLI * names — not credentials) are shown verbatim so a maintainer can locate and * remove them. * * Personal data must be masked here as well as elsewhere: findings are printed * into (potentially public) CI logs, so reporting a leaked name in full would * republish the very data the rule exists to catch. The rule id plus file:line is * enough for a maintainer to find it in the working tree. */ export declare function redactMatch(category: ScanCategory, ruleId: string, match: string): string; /** * Scan a block of text and return redacted findings. `file` is used only for * reporting; it is never read from disk here. */ export declare function scanText(text: string, file?: string): ScanFinding[]; /** * Read a file and scan its contents. * * This used to skip any file whose first 8000 bytes contained a NUL, which made * a NUL byte a silent opt-out from secret and PII scanning: the identical * credential-shaped literal was caught without a NUL and passed with one. Only * genuinely compiled/compressed content is skipped now, decided by magic number * rather than by the presence of a byte, and NULs are stripped during decoding * so they cannot be used to break a pattern apart either. */ export declare function scanFile(path: string, reportedName?: string): ScanFinding[]; /** * Scan a list of files. `nameFor` maps an absolute path to the name reported in * findings (e.g. a repo-relative or package-relative path). */ export declare function scanFiles(paths: string[], nameFor?: (path: string) => string): ScanFinding[]; export interface ScanAllowlistEntry { /** Package-relative file path the exception applies to. */ file: string; /** Rule id the exception applies to. */ ruleId: string; /** Human-readable justification (shown in audits). */ reason: string; } /** * Drop findings that match an explicit, documented allowlist entry. Matching is * exact on (file, ruleId) so an exception can never silently suppress a different * file or a different rule. Returns the surviving findings. */ export declare function applyAllowlist(findings: ScanFinding[], allowlist: ScanAllowlistEntry[]): ScanFinding[]; /** Serialize findings as redacted JSON. Safe to print — contains no secret values. */ export declare function toRedactedJson(findings: ScanFinding[]): string;