@ankurhalder/codebase-snapshot 3.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -310
- package/dist/bin.js +2247 -709
- package/dist/{chunk-5B4UVQLR.js → chunk-RGIMMEAW.js} +1763 -425
- package/dist/cli.cjs +2212 -673
- package/dist/cli.d.cts +3 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +268 -68
- package/dist/index.cjs +1762 -422
- package/dist/index.d.cts +28 -29
- package/dist/index.d.ts +28 -29
- package/dist/index.js +1 -1
- package/dist/{types-CJqkePM4.d.cts → types-LSfwsKE-.d.cts} +38 -4
- package/dist/{types-CJqkePM4.d.ts → types-LSfwsKE-.d.ts} +38 -4
- package/docs/agent.md +77 -37
- package/docs/cli.md +317 -0
- package/docs/configuration.md +254 -226
- package/docs/install.md +114 -86
- package/docs/output.md +213 -0
- package/docs/troubleshooting.md +277 -0
- package/package.json +7 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import * as fs9 from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
3
4
|
import * as path10 from 'path';
|
|
4
5
|
import ts from 'typescript';
|
|
5
6
|
import { pathToFileURL } from 'url';
|
|
@@ -547,14 +548,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
547
548
|
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
548
549
|
return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
549
550
|
};
|
|
550
|
-
const
|
|
551
|
+
const rendersJsx = (node) => {
|
|
552
|
+
const pending = [node];
|
|
553
|
+
while (pending.length > 0) {
|
|
554
|
+
const current = pending.pop();
|
|
555
|
+
if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current) || ts.isJsxFragment(current)) {
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
if (ts.isCallExpression(current)) {
|
|
559
|
+
const callee = current.expression;
|
|
560
|
+
const calleeName = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
561
|
+
if (calleeName === "createElement") return true;
|
|
562
|
+
}
|
|
563
|
+
ts.forEachChild(current, (child) => {
|
|
564
|
+
pending.push(child);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
};
|
|
569
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
570
|
+
const extendsReactBase = (node) => {
|
|
571
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
572
|
+
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword
|
|
573
|
+
);
|
|
574
|
+
const expression = heritage?.types[0]?.expression;
|
|
575
|
+
if (!expression) return false;
|
|
576
|
+
const text2 = ts.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
577
|
+
return REACT_BASE_CLASS.test(text2);
|
|
578
|
+
};
|
|
579
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
551
580
|
summary.functions.push(name);
|
|
552
581
|
if (exported) {
|
|
553
582
|
summary.exports.push(name);
|
|
554
583
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
555
584
|
}
|
|
556
585
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
557
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
586
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
558
587
|
};
|
|
559
588
|
const stack = [sourceFile];
|
|
560
589
|
while (stack.length > 0) {
|
|
@@ -583,7 +612,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
583
612
|
if (ts.isFunctionDeclaration(node)) {
|
|
584
613
|
const exported = isExported(node);
|
|
585
614
|
if (node.name) {
|
|
586
|
-
recordFunctionLike(node.name.text, exported);
|
|
615
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
587
616
|
if (isDefault(node)) summary.exports.push("default");
|
|
588
617
|
} else if (isDefault(node)) {
|
|
589
618
|
summary.exports.push("default");
|
|
@@ -594,7 +623,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
594
623
|
summary.classes.push(node.name.text);
|
|
595
624
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
596
625
|
if (isDefault(node)) summary.exports.push("default");
|
|
597
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
626
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
627
|
+
summary.reactComponents.push(node.name.text);
|
|
628
|
+
}
|
|
598
629
|
} else if (isDefault(node)) {
|
|
599
630
|
summary.exports.push("default");
|
|
600
631
|
}
|
|
@@ -622,7 +653,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
622
653
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
623
654
|
}
|
|
624
655
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
625
|
-
if (/^[A-Z]/.test(name)
|
|
656
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
657
|
+
summary.reactComponents.push(name);
|
|
658
|
+
}
|
|
626
659
|
}
|
|
627
660
|
}
|
|
628
661
|
const children = [];
|
|
@@ -1676,6 +1709,416 @@ function toDetected(preset) {
|
|
|
1676
1709
|
function detectPreset(cwd, forced) {
|
|
1677
1710
|
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
1678
1711
|
}
|
|
1712
|
+
|
|
1713
|
+
// src/security/patterns.ts
|
|
1714
|
+
var STRONG_CREDENTIAL_NAME = "(?:password|passwd|passphrase|pwd|api[_-]?key|api[_-]?secret|secret[_-]?access[_-]?key|secret[_-]?key|client[_-]?secret[_-]?key|client[_-]?secret|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|secret|token)";
|
|
1715
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
1716
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
1717
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1718
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1719
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
1720
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
1721
|
+
{
|
|
1722
|
+
rule: "private-key-header",
|
|
1723
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
rule: "jwt-token",
|
|
1727
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
rule: "aws-access-key-id",
|
|
1731
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
1735
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
1736
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
1737
|
+
rule: "aws-secret-access-key",
|
|
1738
|
+
pattern: new RegExp(
|
|
1739
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
1740
|
+
"i"
|
|
1741
|
+
),
|
|
1742
|
+
valueGroup: 1
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
1746
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
1747
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
1748
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
1749
|
+
// not be pure hex.
|
|
1750
|
+
rule: "aws-secret-access-key-value",
|
|
1751
|
+
pattern: /(?<![A-Za-z0-9/+=])(?![0-9a-fA-F]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{0,39}[A-Z])(?=[A-Za-z0-9/+=]{0,39}[a-z])(?=[A-Za-z0-9/+=]{0,39}[0-9])[A-Za-z0-9/+=]{40}/
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
rule: "github-token",
|
|
1755
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
1759
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
1760
|
+
rule: "slack-token",
|
|
1761
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
1765
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
1766
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
1767
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
1768
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
1769
|
+
rule: "known-vendor-token-prefix",
|
|
1770
|
+
pattern: /\b(?:glpat-[A-Za-z0-9_-]{20,64}|hvs\.[A-Za-z0-9_-]{20,128}|s\.[A-Za-z0-9]{20,64}|whsec_[A-Za-z0-9]{20,64}|dckr_pat_[A-Za-z0-9_-]{20,64}|hf_[A-Za-z0-9]{30,64}|ya29\.[A-Za-z0-9_-]{20,128})/
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
rule: "slack-webhook-url",
|
|
1774
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
rule: "stripe-secret-key",
|
|
1778
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
rule: "google-api-key",
|
|
1782
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
rule: "google-oauth-client-secret",
|
|
1786
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
rule: "openai-anthropic-key",
|
|
1790
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
rule: "npm-token",
|
|
1794
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
rule: "pypi-token",
|
|
1798
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
rule: "sendgrid-key",
|
|
1802
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
rule: "twilio-api-key",
|
|
1806
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
rule: "digitalocean-token",
|
|
1810
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
rule: "azure-storage-account-key",
|
|
1814
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
1818
|
+
//
|
|
1819
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
1820
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
1821
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
1822
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
1823
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
1824
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
1825
|
+
rule: "uri-credentials",
|
|
1826
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
1830
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
1831
|
+
// the string the URL was written in.
|
|
1832
|
+
rule: "database-uri-credentials",
|
|
1833
|
+
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse|mssql|sqlserver|oracle):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
rule: "basic-auth-url",
|
|
1837
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
1841
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
1842
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
1843
|
+
//
|
|
1844
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
1845
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
1846
|
+
// previous string's closing quote stand in for the key's, so
|
|
1847
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
1848
|
+
// the key name.
|
|
1849
|
+
rule: "credential-assignment",
|
|
1850
|
+
pattern: new RegExp(
|
|
1851
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
1852
|
+
"i"
|
|
1853
|
+
),
|
|
1854
|
+
valueGroup: 2
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1858
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1859
|
+
rule: "credential-assignment-weak",
|
|
1860
|
+
pattern: new RegExp(
|
|
1861
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1862
|
+
"i"
|
|
1863
|
+
),
|
|
1864
|
+
valueGroup: 2
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1868
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1869
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1870
|
+
rule: "credential-assignment-unquoted",
|
|
1871
|
+
pattern: new RegExp(
|
|
1872
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1873
|
+
"i"
|
|
1874
|
+
),
|
|
1875
|
+
valueGroup: 1
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1879
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1880
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1881
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1882
|
+
pattern: new RegExp(
|
|
1883
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1884
|
+
"i"
|
|
1885
|
+
),
|
|
1886
|
+
valueGroup: 1
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1890
|
+
//
|
|
1891
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1892
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1893
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1894
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1895
|
+
rule: "authorization-header",
|
|
1896
|
+
pattern: /(?<=(?:Bearer|Basic)\s{1,8})(?:[A-Za-z0-9_.+/-]{0,31}[0-9][A-Za-z0-9_.+/-]{0,1024}|[A-Za-z0-9_.+/-]{32,1024}|[A-Za-z0-9_.+/-]{8,1024}={1,2})(?![A-Za-z0-9_.+/=-])/
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1900
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1901
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1902
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1903
|
+
rule: "generic-secret-assignment",
|
|
1904
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1905
|
+
valueGroup: 1
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1909
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1910
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1911
|
+
//
|
|
1912
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1913
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1914
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1915
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1916
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1917
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1918
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1919
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1920
|
+
rule: "env-secret-assignment",
|
|
1921
|
+
pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?!["'`$<{[\]\s])([^\s#;"'`)]{1,512})/i,
|
|
1922
|
+
valueGroup: 1
|
|
1923
|
+
}
|
|
1924
|
+
];
|
|
1925
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1926
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1927
|
+
|
|
1928
|
+
// src/security/redact.ts
|
|
1929
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1930
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1931
|
+
function describeValue(value) {
|
|
1932
|
+
if (value === null) return "null";
|
|
1933
|
+
if (Array.isArray(value)) return "an array";
|
|
1934
|
+
if (value instanceof RegExp) return value.toString();
|
|
1935
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1936
|
+
return `a ${typeof value}`;
|
|
1937
|
+
}
|
|
1938
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1939
|
+
if (pattern instanceof RegExp) {
|
|
1940
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1941
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1942
|
+
return {
|
|
1943
|
+
rule,
|
|
1944
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1945
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
if (typeof pattern === "string") {
|
|
1949
|
+
try {
|
|
1950
|
+
return {
|
|
1951
|
+
rule,
|
|
1952
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1953
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1954
|
+
};
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
throw new Error(
|
|
1957
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
function compileSecretRules(rules) {
|
|
1966
|
+
if (!Array.isArray(rules)) {
|
|
1967
|
+
throw new Error(
|
|
1968
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
let generated = 0;
|
|
1972
|
+
return rules.map((entry, index) => {
|
|
1973
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
1974
|
+
if (entry instanceof RegExp) {
|
|
1975
|
+
generated += 1;
|
|
1976
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1977
|
+
}
|
|
1978
|
+
if (typeof entry === "string") {
|
|
1979
|
+
generated += 1;
|
|
1980
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1981
|
+
}
|
|
1982
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1983
|
+
const candidate = entry;
|
|
1984
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`${where} is missing \`rule\` (received keys: ${Object.keys(candidate).join(", ") || "none"}). A named rule is { rule: "corp-token", pattern: /CORP-[A-Z0-9]{8}/ }, or pass the RegExp itself.`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
const valueGroup = candidate.valueGroup;
|
|
1990
|
+
if (valueGroup !== void 0) {
|
|
1991
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (candidate.pattern === void 0) {
|
|
1998
|
+
throw new Error(
|
|
1999
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
return compilePattern(
|
|
2003
|
+
candidate.rule,
|
|
2004
|
+
candidate.pattern,
|
|
2005
|
+
valueGroup
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
2010
|
+
);
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
function toGlobal(rule) {
|
|
2014
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
2015
|
+
flags.add("g");
|
|
2016
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
2017
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
2018
|
+
}
|
|
2019
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
2020
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
2021
|
+
const findings = [];
|
|
2022
|
+
let redacted = content;
|
|
2023
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
2024
|
+
blockPattern.lastIndex = 0;
|
|
2025
|
+
let match;
|
|
2026
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
2027
|
+
if (match[0].length === 0) {
|
|
2028
|
+
blockPattern.lastIndex += 1;
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
2032
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
2033
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
2034
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
2035
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
const compiled = rules.map((rule) => ({
|
|
2039
|
+
rule: rule.rule,
|
|
2040
|
+
pattern: toGlobal(rule),
|
|
2041
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
2042
|
+
}));
|
|
2043
|
+
const segments = redacted.split(LINE_BREAK);
|
|
2044
|
+
let lineNumber = 1;
|
|
2045
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
2046
|
+
const line = segments[index];
|
|
2047
|
+
if (line.length > 0) {
|
|
2048
|
+
const spans = [];
|
|
2049
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
2050
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
2051
|
+
pattern.lastIndex = 0;
|
|
2052
|
+
let match;
|
|
2053
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
2054
|
+
if (match[0].length === 0) {
|
|
2055
|
+
pattern.lastIndex += 1;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
2059
|
+
spans.push(
|
|
2060
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
2061
|
+
);
|
|
2062
|
+
matchedRules.add(rule);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
if (spans.length > 0) {
|
|
2066
|
+
for (const rule of matchedRules) {
|
|
2067
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
2068
|
+
}
|
|
2069
|
+
segments[index] = applySpans(line, spans);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
2073
|
+
}
|
|
2074
|
+
return { redacted: segments.join(""), findings };
|
|
2075
|
+
}
|
|
2076
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
2077
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
2078
|
+
function expandOverToken(line, start, end) {
|
|
2079
|
+
let left = start;
|
|
2080
|
+
let right = end;
|
|
2081
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
2082
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
2083
|
+
return { start: left, end: right };
|
|
2084
|
+
}
|
|
2085
|
+
function applySpans(line, spans) {
|
|
2086
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2087
|
+
const merged = [];
|
|
2088
|
+
for (const span of ordered) {
|
|
2089
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
2090
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
2091
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
2092
|
+
const last = merged[merged.length - 1];
|
|
2093
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
2094
|
+
else merged.push({ start, end });
|
|
2095
|
+
}
|
|
2096
|
+
let out = "";
|
|
2097
|
+
let cursor = 0;
|
|
2098
|
+
for (const span of merged) {
|
|
2099
|
+
out += line.slice(cursor, span.start);
|
|
2100
|
+
out += REDACTION_MARKER;
|
|
2101
|
+
cursor = span.end;
|
|
2102
|
+
}
|
|
2103
|
+
out += line.slice(cursor);
|
|
2104
|
+
return out;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/scanner/files.ts
|
|
2108
|
+
var PRESET_IGNORES = new Set(
|
|
2109
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
2110
|
+
);
|
|
2111
|
+
function readLinkTarget(linkPath, fallback) {
|
|
2112
|
+
try {
|
|
2113
|
+
return fs9.readlinkSync(linkPath);
|
|
2114
|
+
} catch {
|
|
2115
|
+
return fallback;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function isInside(root, candidate) {
|
|
2119
|
+
const relative4 = path10.relative(root, candidate);
|
|
2120
|
+
return relative4 === "" || !relative4.startsWith(`..${path10.sep}`) && relative4 !== ".." && !path10.isAbsolute(relative4);
|
|
2121
|
+
}
|
|
1679
2122
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
1680
2123
|
try {
|
|
1681
2124
|
const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -1720,20 +2163,79 @@ var DEFAULT_IGNORED_FILES = [
|
|
|
1720
2163
|
"bun.lockb",
|
|
1721
2164
|
"composer.lock"
|
|
1722
2165
|
];
|
|
1723
|
-
var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.json)?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml))$/i;
|
|
2166
|
+
var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|\.vault-token|vault[-_]agent[-_]token|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.(?:json|ya?ml|toml))?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml|toml)|env|[A-Za-z0-9_-]{1,64}\.env)$/i;
|
|
1724
2167
|
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1725
2168
|
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1726
2169
|
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
2170
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
2171
|
+
function normaliseName(name) {
|
|
2172
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
2173
|
+
let previous = "";
|
|
2174
|
+
while (previous !== normalised) {
|
|
2175
|
+
previous = normalised;
|
|
2176
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
2177
|
+
}
|
|
2178
|
+
return normalised;
|
|
2179
|
+
}
|
|
1727
2180
|
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1728
2181
|
function isSensitivePath(relativePath) {
|
|
1729
2182
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1730
|
-
const
|
|
2183
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
2184
|
+
const base = normaliseName(raw);
|
|
1731
2185
|
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1732
2186
|
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1733
2187
|
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1734
2188
|
}
|
|
1735
2189
|
return matchesSensitiveList(posixPath, base);
|
|
1736
2190
|
}
|
|
2191
|
+
var activeSecretRules = null;
|
|
2192
|
+
function setActiveSecretRules(rules) {
|
|
2193
|
+
activeSecretRules = rules === null ? null : [...rules];
|
|
2194
|
+
}
|
|
2195
|
+
function pathScanRules(extraRules) {
|
|
2196
|
+
const fallback = extraRules ?? activeSecretRules ?? [];
|
|
2197
|
+
return [...DEFAULT_SECRET_PATTERNS, ...fallback];
|
|
2198
|
+
}
|
|
2199
|
+
function findSecretInPath(relativePath, extraRules) {
|
|
2200
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
2201
|
+
const spans = [];
|
|
2202
|
+
const rules = [];
|
|
2203
|
+
for (const { rule, pattern } of pathScanRules(extraRules)) {
|
|
2204
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
2205
|
+
let match;
|
|
2206
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
2207
|
+
if (match[0].length === 0) {
|
|
2208
|
+
scan.lastIndex += 1;
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
2212
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (spans.length === 0) return null;
|
|
2216
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2217
|
+
const merged = [];
|
|
2218
|
+
for (const span of spans) {
|
|
2219
|
+
const last = merged[merged.length - 1];
|
|
2220
|
+
if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
|
|
2221
|
+
else merged.push({ start: span.start, end: span.end });
|
|
2222
|
+
}
|
|
2223
|
+
let out = "";
|
|
2224
|
+
let cursor = 0;
|
|
2225
|
+
for (const span of merged) {
|
|
2226
|
+
out += posixPath.slice(cursor, span.start);
|
|
2227
|
+
out += REDACTION_MARKER;
|
|
2228
|
+
cursor = span.end;
|
|
2229
|
+
}
|
|
2230
|
+
out += posixPath.slice(cursor);
|
|
2231
|
+
return { rules, masked: out };
|
|
2232
|
+
}
|
|
2233
|
+
function maskSkipPaths(records, extraRules) {
|
|
2234
|
+
return records.map((record) => {
|
|
2235
|
+
const hit = findSecretInPath(record.path, extraRules);
|
|
2236
|
+
return hit ? { ...record, path: hit.masked } : record;
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
1737
2239
|
function globToRegExp(glob) {
|
|
1738
2240
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1739
2241
|
const anchored = normalized.startsWith("/");
|
|
@@ -1854,49 +2356,178 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1854
2356
|
}
|
|
1855
2357
|
return null;
|
|
1856
2358
|
}
|
|
1857
|
-
function
|
|
1858
|
-
const includePatterns = options.includePatterns ?? [];
|
|
1859
|
-
const excludePatterns = options.excludePatterns ?? [];
|
|
1860
|
-
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1861
|
-
let candidates;
|
|
1862
|
-
let walkSkips = [];
|
|
2359
|
+
function listIgnoredPaths(cwd) {
|
|
1863
2360
|
try {
|
|
1864
2361
|
const stdout = childProcess.execSync(
|
|
1865
|
-
"git ls-files -z --
|
|
2362
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1866
2363
|
{
|
|
1867
2364
|
cwd,
|
|
1868
2365
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1869
2366
|
encoding: "utf8",
|
|
1870
|
-
maxBuffer:
|
|
2367
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1871
2368
|
}
|
|
1872
2369
|
);
|
|
1873
|
-
|
|
2370
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1874
2371
|
} catch {
|
|
1875
|
-
|
|
1876
|
-
candidates = walked.files;
|
|
1877
|
-
walkSkips = walked.skipped;
|
|
2372
|
+
return [];
|
|
1878
2373
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2374
|
+
}
|
|
2375
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
2376
|
+
function isToolOwnedDirName(name) {
|
|
2377
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
2378
|
+
}
|
|
2379
|
+
function hasSnapshotMarker(target) {
|
|
2380
|
+
try {
|
|
2381
|
+
return fs9.statSync(target).isDirectory() && fs9.existsSync(path10.join(target, ".codebase-snapshot"));
|
|
2382
|
+
} catch {
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function makeToolOwnedDecider(cwd) {
|
|
2387
|
+
const markers = /* @__PURE__ */ new Map();
|
|
2388
|
+
return (relativePath) => {
|
|
2389
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
2390
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
2391
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2392
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
2393
|
+
let hasMarker = markers.get(prefix);
|
|
2394
|
+
if (hasMarker === void 0) {
|
|
2395
|
+
hasMarker = fs9.existsSync(path10.join(cwd, prefix, ".codebase-snapshot"));
|
|
2396
|
+
markers.set(prefix, hasMarker);
|
|
2397
|
+
}
|
|
2398
|
+
if (hasMarker) return true;
|
|
2399
|
+
}
|
|
2400
|
+
return false;
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function makeLinkProbe() {
|
|
2404
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2405
|
+
return (absolutePath) => {
|
|
2406
|
+
let known = cache.get(absolutePath);
|
|
2407
|
+
if (known === void 0) {
|
|
2408
|
+
try {
|
|
2409
|
+
known = fs9.lstatSync(absolutePath).isSymbolicLink();
|
|
2410
|
+
} catch {
|
|
2411
|
+
known = false;
|
|
2412
|
+
}
|
|
2413
|
+
cache.set(absolutePath, known);
|
|
2414
|
+
}
|
|
2415
|
+
return known;
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
2419
|
+
const isLink = makeLinkProbe();
|
|
2420
|
+
let rootReal = cwd;
|
|
2421
|
+
try {
|
|
2422
|
+
rootReal = fs9.realpathSync(cwd);
|
|
2423
|
+
} catch {
|
|
2424
|
+
}
|
|
2425
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2426
|
+
const kept = [];
|
|
2427
|
+
for (const candidate of candidates) {
|
|
2428
|
+
const parts = candidate.split("/");
|
|
2429
|
+
let linkedDir = null;
|
|
2430
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
2431
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2432
|
+
if (isLink(path10.join(cwd, prefix))) {
|
|
2433
|
+
linkedDir = prefix;
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (linkedDir === null) {
|
|
2438
|
+
kept.push(candidate);
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (reported.has(linkedDir)) continue;
|
|
2442
|
+
reported.add(linkedDir);
|
|
2443
|
+
const absolute = path10.join(cwd, linkedDir);
|
|
2444
|
+
let targetReal = absolute;
|
|
2445
|
+
try {
|
|
2446
|
+
targetReal = fs9.realpathSync(absolute);
|
|
2447
|
+
} catch {
|
|
2448
|
+
}
|
|
2449
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
2450
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
2451
|
+
skipped.push(
|
|
2452
|
+
isInside(rootReal, targetReal) ? {
|
|
2453
|
+
path: linkedDir,
|
|
2454
|
+
reason: "symlink",
|
|
2455
|
+
detail: `${what} not followed (target: ${target})`
|
|
2456
|
+
} : {
|
|
2457
|
+
path: linkedDir,
|
|
2458
|
+
reason: "outside-root",
|
|
2459
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
return kept;
|
|
2464
|
+
}
|
|
2465
|
+
function collectFiles(cwd, options = {}) {
|
|
2466
|
+
const includePatterns = options.includePatterns ?? [];
|
|
2467
|
+
const excludePatterns = options.excludePatterns ?? [];
|
|
2468
|
+
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
2469
|
+
const pathRules = options.secretRules;
|
|
2470
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
2471
|
+
let candidates;
|
|
2472
|
+
const skipped = [];
|
|
2473
|
+
try {
|
|
2474
|
+
const stdout = childProcess.execSync(
|
|
2475
|
+
"git ls-files -z --cached --others --exclude-standard",
|
|
2476
|
+
{
|
|
2477
|
+
cwd,
|
|
2478
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2479
|
+
encoding: "utf8",
|
|
2480
|
+
maxBuffer: 50 * 1024 * 1024
|
|
2481
|
+
}
|
|
2482
|
+
);
|
|
2483
|
+
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
2484
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
2485
|
+
const known = new Set(candidates);
|
|
2486
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
2487
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
2488
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
2489
|
+
if (isToolOwned(trimmed)) continue;
|
|
2490
|
+
if (isSensitivePath(trimmed)) {
|
|
2491
|
+
skipped.push({
|
|
2492
|
+
path: trimmed,
|
|
2493
|
+
reason: "sensitive-file",
|
|
2494
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
2495
|
+
});
|
|
2496
|
+
} else {
|
|
2497
|
+
skipped.push({
|
|
2498
|
+
path: trimmed,
|
|
2499
|
+
reason: "gitignore",
|
|
2500
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
} catch {
|
|
2505
|
+
const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
|
|
2506
|
+
candidates = walked.files;
|
|
2507
|
+
skipped.push(...walked.skipped);
|
|
2508
|
+
}
|
|
2509
|
+
const files = candidates.filter((file) => {
|
|
2510
|
+
if (isToolOwned(file)) return false;
|
|
2511
|
+
const ignored = defaultIgnoreReason(file);
|
|
2512
|
+
if (ignored) {
|
|
2513
|
+
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
2516
|
+
if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
|
|
2517
|
+
skipped.push({
|
|
2518
|
+
path: file,
|
|
2519
|
+
reason: "excluded",
|
|
1890
2520
|
detail: `does not match any include pattern (${includePatterns.join(", ")})`
|
|
1891
2521
|
});
|
|
1892
2522
|
return false;
|
|
1893
2523
|
}
|
|
1894
2524
|
for (const pattern of excludePatterns) {
|
|
1895
2525
|
if (matchesExclude(file, pattern)) {
|
|
2526
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1896
2527
|
skipped.push({
|
|
1897
2528
|
path: file,
|
|
1898
|
-
reason: "excluded",
|
|
1899
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
2529
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
2530
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1900
2531
|
});
|
|
1901
2532
|
return false;
|
|
1902
2533
|
}
|
|
@@ -1909,56 +2540,119 @@ function collectFiles(cwd, options = {}) {
|
|
|
1909
2540
|
});
|
|
1910
2541
|
return false;
|
|
1911
2542
|
}
|
|
2543
|
+
const secret = findSecretInPath(file, pathRules);
|
|
2544
|
+
if (secret) {
|
|
2545
|
+
skipped.push({
|
|
2546
|
+
path: secret.masked,
|
|
2547
|
+
reason: "sensitive-path",
|
|
2548
|
+
detail: `path matches secret rule "${secret.rules.join('", "')}"; the file was skipped and its path is masked here because paths are emitted into every artifact`
|
|
2549
|
+
});
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
1912
2552
|
return true;
|
|
1913
2553
|
}).sort();
|
|
1914
|
-
return { files, skipped };
|
|
2554
|
+
return { files, skipped: maskSkipPaths(skipped, pathRules) };
|
|
1915
2555
|
}
|
|
1916
|
-
function
|
|
1917
|
-
const results = { files: [], skipped: [] };
|
|
1918
|
-
let realDir;
|
|
2556
|
+
function rootRealFor(root) {
|
|
1919
2557
|
try {
|
|
1920
|
-
|
|
2558
|
+
return fs9.realpathSync(root);
|
|
1921
2559
|
} catch {
|
|
1922
|
-
|
|
2560
|
+
return root;
|
|
1923
2561
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
const
|
|
2562
|
+
}
|
|
2563
|
+
function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
|
|
2564
|
+
const results = { files: [], skipped: [] };
|
|
2565
|
+
if (visited.has(dir)) return results;
|
|
2566
|
+
visited.add(dir);
|
|
2567
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1927
2568
|
let entries;
|
|
1928
2569
|
try {
|
|
1929
2570
|
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
1930
2571
|
} catch {
|
|
1931
|
-
results.skipped.push({
|
|
2572
|
+
results.skipped.push({
|
|
2573
|
+
path: path10.relative(baseDir, dir).replace(/\\/g, "/") || ".",
|
|
2574
|
+
reason: "read-error",
|
|
2575
|
+
detail: "directory unreadable"
|
|
2576
|
+
});
|
|
1932
2577
|
return results;
|
|
1933
2578
|
}
|
|
1934
2579
|
for (const entry of entries) {
|
|
1935
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1936
2580
|
const fullPath = path10.join(dir, entry.name);
|
|
1937
2581
|
const relPath = path10.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1938
|
-
if (
|
|
2582
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
2583
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
2584
|
+
results.skipped.push({
|
|
2585
|
+
path: relPath,
|
|
2586
|
+
reason: "default-ignore",
|
|
2587
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
2588
|
+
});
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
2592
|
+
results.skipped.push({
|
|
2593
|
+
path: relPath,
|
|
2594
|
+
reason: "default-ignore",
|
|
2595
|
+
detail: "built-in ignore: filename"
|
|
2596
|
+
});
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
if (ignoredByGitignore(relPath)) {
|
|
2600
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
2601
|
+
results.skipped.push({
|
|
2602
|
+
path: relPath,
|
|
2603
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
2604
|
+
detail: "ignored by .gitignore"
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
1939
2609
|
let isDirectory = entry.isDirectory();
|
|
1940
2610
|
let isFile = entry.isFile();
|
|
1941
2611
|
if (entry.isSymbolicLink()) {
|
|
2612
|
+
let targetReal;
|
|
1942
2613
|
try {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2614
|
+
targetReal = fs9.realpathSync(fullPath);
|
|
2615
|
+
} catch {
|
|
2616
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
let stats;
|
|
2620
|
+
try {
|
|
2621
|
+
stats = fs9.statSync(fullPath);
|
|
2622
|
+
} catch {
|
|
2623
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
2627
|
+
if (!isInside(rootReal, targetReal)) {
|
|
2628
|
+
results.skipped.push({
|
|
2629
|
+
path: relPath,
|
|
2630
|
+
reason: "outside-root",
|
|
2631
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
2632
|
+
});
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
if (stats.isDirectory()) {
|
|
1950
2636
|
results.skipped.push({
|
|
1951
2637
|
path: relPath,
|
|
1952
2638
|
reason: "symlink",
|
|
1953
|
-
detail:
|
|
2639
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
2640
|
+
});
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
if (!stats.isFile()) {
|
|
2644
|
+
results.skipped.push({
|
|
2645
|
+
path: relPath,
|
|
2646
|
+
reason: "symlink",
|
|
2647
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1954
2648
|
});
|
|
1955
|
-
} catch {
|
|
1956
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1957
2649
|
continue;
|
|
1958
2650
|
}
|
|
2651
|
+
isDirectory = false;
|
|
2652
|
+
isFile = true;
|
|
1959
2653
|
}
|
|
1960
2654
|
if (isDirectory) {
|
|
1961
|
-
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
2655
|
+
const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
|
|
1962
2656
|
results.files.push(...nested.files);
|
|
1963
2657
|
results.skipped.push(...nested.skipped);
|
|
1964
2658
|
} else if (isFile) {
|
|
@@ -1968,195 +2662,6 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1968
2662
|
return results;
|
|
1969
2663
|
}
|
|
1970
2664
|
|
|
1971
|
-
// src/security/patterns.ts
|
|
1972
|
-
var DEFAULT_SECRET_PATTERNS = [
|
|
1973
|
-
{
|
|
1974
|
-
rule: "private-key-header",
|
|
1975
|
-
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1976
|
-
},
|
|
1977
|
-
{
|
|
1978
|
-
rule: "jwt-token",
|
|
1979
|
-
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1980
|
-
},
|
|
1981
|
-
{
|
|
1982
|
-
rule: "aws-access-key-id",
|
|
1983
|
-
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1984
|
-
},
|
|
1985
|
-
{
|
|
1986
|
-
rule: "aws-secret-access-key",
|
|
1987
|
-
pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
|
|
1988
|
-
},
|
|
1989
|
-
{
|
|
1990
|
-
rule: "github-token",
|
|
1991
|
-
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1992
|
-
},
|
|
1993
|
-
{
|
|
1994
|
-
rule: "slack-token",
|
|
1995
|
-
pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
|
|
1996
|
-
},
|
|
1997
|
-
{
|
|
1998
|
-
rule: "slack-webhook-url",
|
|
1999
|
-
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
2000
|
-
},
|
|
2001
|
-
{
|
|
2002
|
-
rule: "stripe-secret-key",
|
|
2003
|
-
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
2004
|
-
},
|
|
2005
|
-
{
|
|
2006
|
-
rule: "google-api-key",
|
|
2007
|
-
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
2008
|
-
},
|
|
2009
|
-
{
|
|
2010
|
-
rule: "google-oauth-client-secret",
|
|
2011
|
-
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
2012
|
-
},
|
|
2013
|
-
{
|
|
2014
|
-
rule: "openai-anthropic-key",
|
|
2015
|
-
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
2016
|
-
},
|
|
2017
|
-
{
|
|
2018
|
-
rule: "npm-token",
|
|
2019
|
-
pattern: /npm_[A-Za-z0-9]{36}/
|
|
2020
|
-
},
|
|
2021
|
-
{
|
|
2022
|
-
rule: "pypi-token",
|
|
2023
|
-
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
rule: "sendgrid-key",
|
|
2027
|
-
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
2028
|
-
},
|
|
2029
|
-
{
|
|
2030
|
-
rule: "twilio-api-key",
|
|
2031
|
-
pattern: /SK[0-9a-fA-F]{32}/
|
|
2032
|
-
},
|
|
2033
|
-
{
|
|
2034
|
-
rule: "digitalocean-token",
|
|
2035
|
-
pattern: /dop_v1_[0-9a-f]{64}/
|
|
2036
|
-
},
|
|
2037
|
-
{
|
|
2038
|
-
rule: "azure-storage-account-key",
|
|
2039
|
-
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
2040
|
-
},
|
|
2041
|
-
{
|
|
2042
|
-
rule: "database-uri-credentials",
|
|
2043
|
-
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
|
|
2044
|
-
},
|
|
2045
|
-
{
|
|
2046
|
-
rule: "basic-auth-url",
|
|
2047
|
-
pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
|
|
2048
|
-
},
|
|
2049
|
-
{
|
|
2050
|
-
rule: "generic-secret-assignment",
|
|
2051
|
-
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
|
|
2052
|
-
},
|
|
2053
|
-
{
|
|
2054
|
-
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
2055
|
-
// was therefore invisible to the rule above. Anchored to the line start and
|
|
2056
|
-
// limited to secret-looking names to stay at zero false positives.
|
|
2057
|
-
//
|
|
2058
|
-
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
2059
|
-
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
2060
|
-
// every offset of a long identifier, which made this rule quadratic
|
|
2061
|
-
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
2062
|
-
rule: "env-secret-assignment",
|
|
2063
|
-
pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?![\s$<{[])[^\s#]{6,512}$/i
|
|
2064
|
-
}
|
|
2065
|
-
];
|
|
2066
|
-
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
2067
|
-
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
2068
|
-
|
|
2069
|
-
// src/security/redact.ts
|
|
2070
|
-
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
2071
|
-
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
2072
|
-
function compileSecretRules(rules) {
|
|
2073
|
-
return rules.map((rule) => {
|
|
2074
|
-
if (rule.pattern instanceof RegExp) {
|
|
2075
|
-
return {
|
|
2076
|
-
rule: rule.rule,
|
|
2077
|
-
pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
if (typeof rule.pattern === "string") {
|
|
2081
|
-
try {
|
|
2082
|
-
return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
throw new Error(
|
|
2085
|
-
`Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
|
|
2086
|
-
);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
throw new Error(
|
|
2090
|
-
`Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
|
|
2091
|
-
);
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
function toGlobal(pattern) {
|
|
2095
|
-
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
2096
|
-
return new RegExp(pattern.source, `${flags}g`);
|
|
2097
|
-
}
|
|
2098
|
-
function scanAndRedactSecrets(content, rules, filePath) {
|
|
2099
|
-
const findings = [];
|
|
2100
|
-
let redacted = content;
|
|
2101
|
-
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
2102
|
-
blockPattern.lastIndex = 0;
|
|
2103
|
-
let match;
|
|
2104
|
-
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
2105
|
-
if (match[0].length === 0) {
|
|
2106
|
-
blockPattern.lastIndex += 1;
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
const line = redacted.slice(0, match.index).split("\n").length;
|
|
2110
|
-
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
2111
|
-
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
2112
|
-
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
2113
|
-
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
const compiled = rules.map((rule) => ({
|
|
2117
|
-
rule: rule.rule,
|
|
2118
|
-
pattern: toGlobal(rule.pattern)
|
|
2119
|
-
}));
|
|
2120
|
-
const lines = redacted.split("\n");
|
|
2121
|
-
const redactedLines = lines.map((line, index) => {
|
|
2122
|
-
if (line.length === 0) return line;
|
|
2123
|
-
const spans = [];
|
|
2124
|
-
const matchedRules = /* @__PURE__ */ new Set();
|
|
2125
|
-
for (const { rule, pattern } of compiled) {
|
|
2126
|
-
pattern.lastIndex = 0;
|
|
2127
|
-
let match;
|
|
2128
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
2129
|
-
if (match[0].length === 0) {
|
|
2130
|
-
pattern.lastIndex += 1;
|
|
2131
|
-
continue;
|
|
2132
|
-
}
|
|
2133
|
-
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
2134
|
-
matchedRules.add(rule);
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
if (spans.length === 0) return line;
|
|
2138
|
-
for (const rule of matchedRules) {
|
|
2139
|
-
findings.push({ rule, line: index + 1, path: filePath });
|
|
2140
|
-
}
|
|
2141
|
-
return applySpans(line, spans);
|
|
2142
|
-
});
|
|
2143
|
-
return { redacted: redactedLines.join("\n"), findings };
|
|
2144
|
-
}
|
|
2145
|
-
function applySpans(line, spans) {
|
|
2146
|
-
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2147
|
-
let out = "";
|
|
2148
|
-
let cursor = 0;
|
|
2149
|
-
for (const span of ordered) {
|
|
2150
|
-
if (span.end <= cursor) continue;
|
|
2151
|
-
const start = Math.max(span.start, cursor);
|
|
2152
|
-
out += line.slice(cursor, start);
|
|
2153
|
-
out += REDACTION_MARKER;
|
|
2154
|
-
cursor = Math.min(span.end, line.length);
|
|
2155
|
-
}
|
|
2156
|
-
out += line.slice(cursor);
|
|
2157
|
-
return out;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
2665
|
// src/config.ts
|
|
2161
2666
|
var CONFIG_FILENAMES = [
|
|
2162
2667
|
"snapshot.config.ts",
|
|
@@ -2206,7 +2711,12 @@ function normalizeResolvedConfig(config) {
|
|
|
2206
2711
|
),
|
|
2207
2712
|
include: resolveInclude(config.include),
|
|
2208
2713
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
2209
|
-
presetExclude:
|
|
2714
|
+
presetExclude: [
|
|
2715
|
+
.../* @__PURE__ */ new Set([
|
|
2716
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
2717
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
2718
|
+
])
|
|
2719
|
+
],
|
|
2210
2720
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
2211
2721
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
2212
2722
|
hooks: resolveHooks(config.hooks),
|
|
@@ -2243,7 +2753,68 @@ function supportsTypeScriptImports() {
|
|
|
2243
2753
|
return major > 22 || major === 22 && minor >= 6;
|
|
2244
2754
|
}
|
|
2245
2755
|
function stripJsonComments2(raw) {
|
|
2246
|
-
|
|
2756
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2757
|
+
let out = "";
|
|
2758
|
+
let inString = false;
|
|
2759
|
+
let escaped = false;
|
|
2760
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2761
|
+
const char = text2[index];
|
|
2762
|
+
if (inString) {
|
|
2763
|
+
out += char;
|
|
2764
|
+
if (escaped) escaped = false;
|
|
2765
|
+
else if (char === "\\") escaped = true;
|
|
2766
|
+
else if (char === '"') inString = false;
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (char === '"') {
|
|
2770
|
+
inString = true;
|
|
2771
|
+
out += char;
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2775
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2776
|
+
index -= 1;
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2780
|
+
index += 2;
|
|
2781
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2782
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2783
|
+
index += 1;
|
|
2784
|
+
}
|
|
2785
|
+
index += 1;
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
out += char;
|
|
2789
|
+
}
|
|
2790
|
+
return stripTrailingCommas(out);
|
|
2791
|
+
}
|
|
2792
|
+
function stripTrailingCommas(text2) {
|
|
2793
|
+
let out = "";
|
|
2794
|
+
let inString = false;
|
|
2795
|
+
let escaped = false;
|
|
2796
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2797
|
+
const char = text2[index];
|
|
2798
|
+
if (inString) {
|
|
2799
|
+
out += char;
|
|
2800
|
+
if (escaped) escaped = false;
|
|
2801
|
+
else if (char === "\\") escaped = true;
|
|
2802
|
+
else if (char === '"') inString = false;
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
if (char === '"') {
|
|
2806
|
+
inString = true;
|
|
2807
|
+
out += char;
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (char === ",") {
|
|
2811
|
+
let next = index + 1;
|
|
2812
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2813
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2814
|
+
}
|
|
2815
|
+
out += char;
|
|
2816
|
+
}
|
|
2817
|
+
return out;
|
|
2247
2818
|
}
|
|
2248
2819
|
function parseJsonish(raw, file) {
|
|
2249
2820
|
try {
|
|
@@ -2252,6 +2823,47 @@ function parseJsonish(raw, file) {
|
|
|
2252
2823
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
2253
2824
|
}
|
|
2254
2825
|
}
|
|
2826
|
+
function describeValue2(value) {
|
|
2827
|
+
if (value === null) return "null";
|
|
2828
|
+
if (Array.isArray(value)) return "an array";
|
|
2829
|
+
if (value instanceof RegExp) return value.toString();
|
|
2830
|
+
if (typeof value === "function") return "a function";
|
|
2831
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2832
|
+
if (typeof value === "object") {
|
|
2833
|
+
const keys = Object.keys(value);
|
|
2834
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2835
|
+
}
|
|
2836
|
+
return `${typeof value} (${String(value)})`;
|
|
2837
|
+
}
|
|
2838
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2839
|
+
function readConfigExport(exported, file) {
|
|
2840
|
+
if (exported === void 0 || exported === null) {
|
|
2841
|
+
throw new Error(
|
|
2842
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
if (Array.isArray(exported)) {
|
|
2846
|
+
throw new Error(
|
|
2847
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
if (typeof exported === "function") {
|
|
2851
|
+
throw new Error(
|
|
2852
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
if (typeof exported !== "object") {
|
|
2856
|
+
throw new Error(
|
|
2857
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
if (isThenable(exported)) {
|
|
2861
|
+
throw new Error(
|
|
2862
|
+
`${file} exports a Promise/thenable, not a configuration object. An async config is read synchronously, so its settings would be ignored: export a plain object (\`const config = {\u2026}; export default config;\`).`
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
return exported;
|
|
2866
|
+
}
|
|
2255
2867
|
async function loadConfigFileDetailed(cwd) {
|
|
2256
2868
|
const warnings = [];
|
|
2257
2869
|
const errors = [];
|
|
@@ -2281,13 +2893,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
2281
2893
|
}
|
|
2282
2894
|
try {
|
|
2283
2895
|
const imported = await import(pathToFileURL(fullPath).href);
|
|
2284
|
-
const config = imported.default ?? imported;
|
|
2285
|
-
if (!config || typeof config !== "object") {
|
|
2286
|
-
const message = `${fullPath} does not export a configuration object.`;
|
|
2287
|
-
warnings.push(message);
|
|
2288
|
-
errors.push(message);
|
|
2289
|
-
continue;
|
|
2290
|
-
}
|
|
2896
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
2291
2897
|
return { config, file: fullPath, warnings, errors };
|
|
2292
2898
|
} catch (err) {
|
|
2293
2899
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -2341,7 +2947,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
2341
2947
|
function isArtifactName(value) {
|
|
2342
2948
|
return ARTIFACT_NAMES.includes(value);
|
|
2343
2949
|
}
|
|
2344
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2950
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
2345
2951
|
const artifacts = {
|
|
2346
2952
|
...DEFAULT_ARTIFACTS,
|
|
2347
2953
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -2363,6 +2969,9 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
2363
2969
|
artifacts[name] = value;
|
|
2364
2970
|
}
|
|
2365
2971
|
}
|
|
2972
|
+
if (monolithOverride !== void 0) {
|
|
2973
|
+
artifacts.monolith = monolithOverride;
|
|
2974
|
+
}
|
|
2366
2975
|
for (const name of skipped ?? []) {
|
|
2367
2976
|
if (!isArtifactName(name)) {
|
|
2368
2977
|
throw new Error(
|
|
@@ -2373,9 +2982,106 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
2373
2982
|
}
|
|
2374
2983
|
return artifacts;
|
|
2375
2984
|
}
|
|
2376
|
-
function
|
|
2377
|
-
if (
|
|
2378
|
-
|
|
2985
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2986
|
+
if (value === void 0) return fallback;
|
|
2987
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2988
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
2989
|
+
}
|
|
2990
|
+
return value;
|
|
2991
|
+
}
|
|
2992
|
+
function strictBoolean(value, fallback, label) {
|
|
2993
|
+
if (value === void 0) return fallback;
|
|
2994
|
+
if (typeof value !== "boolean") {
|
|
2995
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
2996
|
+
}
|
|
2997
|
+
return value;
|
|
2998
|
+
}
|
|
2999
|
+
function strictString(value, fallback, label) {
|
|
3000
|
+
if (value === void 0) return fallback;
|
|
3001
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3002
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
3003
|
+
}
|
|
3004
|
+
return value;
|
|
3005
|
+
}
|
|
3006
|
+
function resolveExcludePatterns(value) {
|
|
3007
|
+
if (value === void 0) return [];
|
|
3008
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
3014
|
+
}
|
|
3015
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
3016
|
+
function resolvePresetName(value) {
|
|
3017
|
+
if (value === void 0) return void 0;
|
|
3018
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3019
|
+
throw new Error(
|
|
3020
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
const name = value.trim();
|
|
3024
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
3025
|
+
}
|
|
3026
|
+
function resolveRuleList(value, kind) {
|
|
3027
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
3028
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
3029
|
+
if (value === void 0) return [];
|
|
3030
|
+
if (!Array.isArray(value)) {
|
|
3031
|
+
throw new Error(
|
|
3032
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
3035
|
+
return value.map((entry, index) => {
|
|
3036
|
+
const where = `${collection}[${index}]`;
|
|
3037
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
3038
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
3039
|
+
}
|
|
3040
|
+
const record = entry;
|
|
3041
|
+
const keys = Object.keys(record);
|
|
3042
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
3043
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
3044
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
3045
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
3046
|
+
throw new Error(
|
|
3047
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
const match = record.match;
|
|
3051
|
+
if (match === void 0) {
|
|
3052
|
+
throw new Error(
|
|
3053
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
3057
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
3058
|
+
}
|
|
3059
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
3060
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
3061
|
+
}
|
|
3062
|
+
return {
|
|
3063
|
+
name: rawName.trim(),
|
|
3064
|
+
match
|
|
3065
|
+
};
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
function resolveLayerRules(value) {
|
|
3069
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
3070
|
+
}
|
|
3071
|
+
function resolveKindRules(value) {
|
|
3072
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
3073
|
+
}
|
|
3074
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
3075
|
+
const patterns = [];
|
|
3076
|
+
for (const dir of [outDir, tmpDir]) {
|
|
3077
|
+
const absolute = path10.resolve(cwd, dir);
|
|
3078
|
+
const relative4 = path10.relative(cwd, absolute);
|
|
3079
|
+
if (!relative4 || relative4 === ".." || relative4.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative4)) {
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
patterns.push(`${relative4.replace(/\\/g, "/")}/**`);
|
|
3083
|
+
}
|
|
3084
|
+
return patterns;
|
|
2379
3085
|
}
|
|
2380
3086
|
var BYTE_UNITS = {
|
|
2381
3087
|
b: 1,
|
|
@@ -2444,11 +3150,13 @@ function resolveLayersMode(mode) {
|
|
|
2444
3150
|
}
|
|
2445
3151
|
return mode;
|
|
2446
3152
|
}
|
|
2447
|
-
function resolveBudgets(budgets, fallback) {
|
|
3153
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
2448
3154
|
const byLayer = {};
|
|
2449
3155
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
2450
3156
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
2451
|
-
throw new Error(
|
|
3157
|
+
throw new Error(
|
|
3158
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
3159
|
+
);
|
|
2452
3160
|
}
|
|
2453
3161
|
if (budgets.byLayer !== void 0) {
|
|
2454
3162
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -2463,10 +3171,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
2463
3171
|
);
|
|
2464
3172
|
}
|
|
2465
3173
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3174
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
3175
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
3176
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
3177
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
3178
|
+
}
|
|
3179
|
+
return { default: fallback, byLayer };
|
|
2470
3180
|
}
|
|
2471
3181
|
function resolveHooks(hooks) {
|
|
2472
3182
|
if (hooks === void 0) return {};
|
|
@@ -2479,6 +3189,11 @@ function resolveHooks(hooks) {
|
|
|
2479
3189
|
return { ...hooks };
|
|
2480
3190
|
}
|
|
2481
3191
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
3192
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
3193
|
+
throw new Error(
|
|
3194
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
2482
3197
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
2483
3198
|
const resolved = path10.resolve(candidate);
|
|
2484
3199
|
if (!fs9.existsSync(resolved)) {
|
|
@@ -2491,55 +3206,107 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
2491
3206
|
}
|
|
2492
3207
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
2493
3208
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
2494
|
-
const forcedPreset = userConfig?.preset;
|
|
3209
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
2495
3210
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
2496
3211
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
2497
3212
|
}
|
|
2498
3213
|
const preset = detectPreset(cwd, forcedPreset);
|
|
2499
3214
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
2500
|
-
const
|
|
2501
|
-
const
|
|
2502
|
-
const
|
|
2503
|
-
const
|
|
2504
|
-
const
|
|
2505
|
-
|
|
3215
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
3216
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
3217
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
3218
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
3219
|
+
const secrets = userConfig?.secrets;
|
|
3220
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
3221
|
+
throw new Error(
|
|
3222
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
3226
|
+
if (!Array.isArray(customRules)) {
|
|
3227
|
+
throw new Error(
|
|
3228
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
const disableDefaultPatterns = strictBoolean(
|
|
3232
|
+
secrets?.disableDefaultPatterns,
|
|
3233
|
+
false,
|
|
3234
|
+
"`secrets.disableDefaultPatterns`"
|
|
3235
|
+
);
|
|
3236
|
+
const aliasedRules = userConfig?.secretRules;
|
|
3237
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
3238
|
+
throw new Error(
|
|
3239
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
const compiledCustomRules = compileSecretRules([
|
|
3243
|
+
...customRules,
|
|
3244
|
+
...aliasedRules ?? []
|
|
3245
|
+
]);
|
|
3246
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
3247
|
+
setActiveSecretRules(secretRules);
|
|
3248
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
3249
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
2506
3250
|
const artifacts = resolveArtifacts(
|
|
2507
3251
|
userConfig?.artifacts,
|
|
2508
3252
|
cliOptions?.skipArtifacts,
|
|
2509
|
-
includeMonolith
|
|
3253
|
+
includeMonolith,
|
|
3254
|
+
cliMonolith
|
|
2510
3255
|
);
|
|
3256
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
2511
3257
|
const maxPartBytes = boundedBytes(
|
|
2512
|
-
|
|
3258
|
+
explicitMaxPartBytes,
|
|
2513
3259
|
300 * 1024,
|
|
2514
3260
|
MIN_MAX_PART_BYTES,
|
|
2515
3261
|
"maxPartBytes"
|
|
2516
3262
|
);
|
|
2517
|
-
const budgets = resolveBudgets(
|
|
3263
|
+
const budgets = resolveBudgets(
|
|
3264
|
+
userConfig?.budgets,
|
|
3265
|
+
maxPartBytes,
|
|
3266
|
+
explicitMaxPartBytes !== void 0
|
|
3267
|
+
);
|
|
3268
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
3269
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
3270
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
3271
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
3272
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
3273
|
+
userConfig?.includeSensitiveFiles,
|
|
3274
|
+
false,
|
|
3275
|
+
"`includeSensitiveFiles`"
|
|
3276
|
+
);
|
|
3277
|
+
const includeSensitiveFiles = strictBoolean(
|
|
3278
|
+
cliOptions?.includeSensitiveFiles,
|
|
3279
|
+
configuredSensitiveFiles,
|
|
3280
|
+
"`includeSensitiveFiles`"
|
|
3281
|
+
);
|
|
3282
|
+
const presetExclude = [
|
|
3283
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
3284
|
+
];
|
|
2518
3285
|
return markResolvedConfig({
|
|
2519
3286
|
cwd,
|
|
2520
|
-
branch
|
|
2521
|
-
outDir
|
|
2522
|
-
tmpDir
|
|
3287
|
+
branch,
|
|
3288
|
+
outDir,
|
|
3289
|
+
tmpDir,
|
|
2523
3290
|
maxPartBytes,
|
|
2524
3291
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
2525
|
-
previewHeadLines:
|
|
2526
|
-
previewTailLines:
|
|
3292
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
3293
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
2527
3294
|
includeMonolith: artifacts.monolith,
|
|
2528
3295
|
maxMonolithBytes: optionalBytes(
|
|
2529
3296
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
2530
3297
|
"maxMonolithBytes"
|
|
2531
3298
|
),
|
|
2532
|
-
includeSensitiveFiles
|
|
3299
|
+
includeSensitiveFiles,
|
|
2533
3300
|
include: resolveInclude(userConfig?.include),
|
|
2534
3301
|
preset: preset.presetName,
|
|
2535
|
-
presetExclude
|
|
3302
|
+
presetExclude,
|
|
2536
3303
|
artifacts,
|
|
2537
3304
|
layersMode,
|
|
2538
3305
|
budgets,
|
|
2539
3306
|
hooks: resolveHooks(userConfig?.hooks),
|
|
2540
3307
|
layers,
|
|
2541
3308
|
kinds,
|
|
2542
|
-
excludePatterns: userConfig?.excludePatterns
|
|
3309
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
2543
3310
|
secretRules
|
|
2544
3311
|
});
|
|
2545
3312
|
}
|
|
@@ -2585,7 +3352,8 @@ function defineConfig(config) {
|
|
|
2585
3352
|
}
|
|
2586
3353
|
|
|
2587
3354
|
// src/generator/audit.ts
|
|
2588
|
-
function generateAuditReport(manifest, secretFindings) {
|
|
3355
|
+
function generateAuditReport(manifest, secretFindings, secretRules) {
|
|
3356
|
+
const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
|
|
2589
3357
|
const lines = [
|
|
2590
3358
|
"# Codebase Health & Security Audit",
|
|
2591
3359
|
"",
|
|
@@ -2594,7 +3362,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2594
3362
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
2595
3363
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
2596
3364
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
2597
|
-
|
|
3365
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
3366
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
3367
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
3368
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
3369
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
2598
3370
|
"",
|
|
2599
3371
|
"## 1. Security Scan & Redaction",
|
|
2600
3372
|
"",
|
|
@@ -2616,7 +3388,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2616
3388
|
"| :--- | :--- | :--- |"
|
|
2617
3389
|
);
|
|
2618
3390
|
for (const finding of secretFindings) {
|
|
2619
|
-
lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
3391
|
+
lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
2620
3392
|
}
|
|
2621
3393
|
lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
|
|
2622
3394
|
for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
|
|
@@ -2638,7 +3410,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2638
3410
|
for (const file of largeFiles) {
|
|
2639
3411
|
const sizeKb = (file.sizeBytes / 1024).toFixed(1);
|
|
2640
3412
|
const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
|
|
2641
|
-
lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
3413
|
+
lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
2642
3414
|
}
|
|
2643
3415
|
lines.push("");
|
|
2644
3416
|
}
|
|
@@ -2680,7 +3452,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2680
3452
|
"| :--- | :--- | :--- |"
|
|
2681
3453
|
);
|
|
2682
3454
|
for (const skip of manifest.skipped) {
|
|
2683
|
-
lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
3455
|
+
lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
2684
3456
|
}
|
|
2685
3457
|
lines.push("");
|
|
2686
3458
|
}
|
|
@@ -2797,6 +3569,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2797
3569
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
2798
3570
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
2799
3571
|
}
|
|
3572
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
3573
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2800
3574
|
const header = [
|
|
2801
3575
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2802
3576
|
"",
|
|
@@ -2809,9 +3583,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2809
3583
|
"",
|
|
2810
3584
|
"## Files",
|
|
2811
3585
|
"",
|
|
2812
|
-
|
|
2813
|
-
...
|
|
2814
|
-
|
|
3586
|
+
`${listFence}text`,
|
|
3587
|
+
...pathLines,
|
|
3588
|
+
listFence,
|
|
2815
3589
|
"",
|
|
2816
3590
|
"## Contents",
|
|
2817
3591
|
""
|
|
@@ -2839,7 +3613,8 @@ function capValue(value, max) {
|
|
|
2839
3613
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
2840
3614
|
}
|
|
2841
3615
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
2842
|
-
var
|
|
3616
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
3617
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
2843
3618
|
function topEntries(counts, limit) {
|
|
2844
3619
|
return Object.entries(counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, limit).map(([key, value]) => `${capMetadata(key)} (${value})`).join(", ");
|
|
2845
3620
|
}
|
|
@@ -2935,6 +3710,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2935
3710
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
2936
3711
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
2937
3712
|
}
|
|
3713
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
3714
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2938
3715
|
const header = [
|
|
2939
3716
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2940
3717
|
"",
|
|
@@ -2947,9 +3724,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2947
3724
|
"",
|
|
2948
3725
|
"## Files",
|
|
2949
3726
|
"",
|
|
2950
|
-
|
|
2951
|
-
...
|
|
2952
|
-
|
|
3727
|
+
`${listFence}text`,
|
|
3728
|
+
...pathLines,
|
|
3729
|
+
listFence,
|
|
2953
3730
|
"",
|
|
2954
3731
|
"## Contents",
|
|
2955
3732
|
""
|
|
@@ -3008,9 +3785,12 @@ function renderFileSection(file) {
|
|
|
3008
3785
|
].join("\n");
|
|
3009
3786
|
}
|
|
3010
3787
|
function fenceForContent(content) {
|
|
3788
|
+
return fenceForText(content, 4);
|
|
3789
|
+
}
|
|
3790
|
+
function fenceForText(content, minimum) {
|
|
3011
3791
|
const matches = content.match(/`+/g);
|
|
3012
3792
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3013
|
-
return "`".repeat(Math.max(
|
|
3793
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3014
3794
|
}
|
|
3015
3795
|
function renderSummary(summary) {
|
|
3016
3796
|
if (!summary) return [];
|
|
@@ -3159,6 +3939,7 @@ function generateReadme(manifest) {
|
|
|
3159
3939
|
|
|
3160
3940
|
// src/generator/tree.ts
|
|
3161
3941
|
var MAX_TREE_LINES = 2e4;
|
|
3942
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
3162
3943
|
function generateProjectTree(filePaths) {
|
|
3163
3944
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
3164
3945
|
for (const filePath of filePaths) {
|
|
@@ -3173,7 +3954,7 @@ function generateProjectTree(filePaths) {
|
|
|
3173
3954
|
current = child;
|
|
3174
3955
|
}
|
|
3175
3956
|
}
|
|
3176
|
-
const lines = ["
|
|
3957
|
+
const lines = ["."];
|
|
3177
3958
|
let truncatedAt = -1;
|
|
3178
3959
|
function walk(node, prefix) {
|
|
3179
3960
|
if (truncatedAt !== -1) return;
|
|
@@ -3188,7 +3969,8 @@ function generateProjectTree(filePaths) {
|
|
|
3188
3969
|
const isLast = index === keys.length - 1;
|
|
3189
3970
|
const child = node.children.get(key);
|
|
3190
3971
|
const isDirectory = child.children.size > 0;
|
|
3191
|
-
|
|
3972
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
3973
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
3192
3974
|
if (lines.length >= MAX_TREE_LINES) {
|
|
3193
3975
|
truncatedAt = lines.length;
|
|
3194
3976
|
return;
|
|
@@ -3200,8 +3982,8 @@ function generateProjectTree(filePaths) {
|
|
|
3200
3982
|
if (truncatedAt !== -1) {
|
|
3201
3983
|
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
3202
3984
|
}
|
|
3203
|
-
lines.
|
|
3204
|
-
return lines.join("\n");
|
|
3985
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
3986
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
3205
3987
|
}
|
|
3206
3988
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3207
3989
|
".png",
|
|
@@ -3332,6 +4114,9 @@ function detectLanguage(filePath) {
|
|
|
3332
4114
|
const ext = path10.posix.extname(base).toLowerCase();
|
|
3333
4115
|
return LANGUAGE_BY_EXTENSION[ext] ?? "text";
|
|
3334
4116
|
}
|
|
4117
|
+
function maskedPath(value, secretRules) {
|
|
4118
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
4119
|
+
}
|
|
3335
4120
|
function emptyDrift() {
|
|
3336
4121
|
return {
|
|
3337
4122
|
missingInSnapshot: [],
|
|
@@ -3345,6 +4130,15 @@ function emptyDrift() {
|
|
|
3345
4130
|
function sha256Hex(buffer) {
|
|
3346
4131
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
3347
4132
|
}
|
|
4133
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
4134
|
+
const absolutePath = path10.join(cwd, relativePath);
|
|
4135
|
+
try {
|
|
4136
|
+
if (!fs9.statSync(absolutePath).isFile()) return false;
|
|
4137
|
+
return !isBinaryFile(absolutePath);
|
|
4138
|
+
} catch {
|
|
4139
|
+
return false;
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
3348
4142
|
function validateSnapshot(configInput) {
|
|
3349
4143
|
assertUsableConfigInput(configInput);
|
|
3350
4144
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -3393,15 +4187,20 @@ function validateSnapshot(configInput) {
|
|
|
3393
4187
|
const skippedSet = new Set(
|
|
3394
4188
|
manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
|
|
3395
4189
|
);
|
|
4190
|
+
const isSkippedOnDisk = (diskFile) => {
|
|
4191
|
+
if (skippedSet.has(diskFile)) return true;
|
|
4192
|
+
return skippedSet.has(maskedPath(diskFile, config.secretRules));
|
|
4193
|
+
};
|
|
3396
4194
|
const collection = collectFiles(config.cwd, {
|
|
3397
4195
|
includePatterns: config.include,
|
|
3398
4196
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
3399
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4197
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
4198
|
+
secretRules: config.secretRules
|
|
3400
4199
|
});
|
|
3401
4200
|
for (const diskFile of collection.files) {
|
|
3402
|
-
if (skippedSet.has(diskFile)) continue;
|
|
3403
4201
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
3404
4202
|
if (!snapshotHash) {
|
|
4203
|
+
if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
3405
4204
|
drift.missingInSnapshot.push(diskFile);
|
|
3406
4205
|
continue;
|
|
3407
4206
|
}
|
|
@@ -3670,10 +4469,19 @@ function validateSnapshot(configInput) {
|
|
|
3670
4469
|
`${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
|
|
3671
4470
|
);
|
|
3672
4471
|
}
|
|
4472
|
+
const maskedDrift = {
|
|
4473
|
+
...drift,
|
|
4474
|
+
missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
|
|
4475
|
+
missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
|
|
4476
|
+
hashMismatches: drift.hashMismatches.map((mismatch) => ({
|
|
4477
|
+
...mismatch,
|
|
4478
|
+
path: maskedPath(mismatch.path, config.secretRules)
|
|
4479
|
+
}))
|
|
4480
|
+
};
|
|
3673
4481
|
return {
|
|
3674
4482
|
valid: errors.length === 0,
|
|
3675
4483
|
manifestPresent: true,
|
|
3676
|
-
drift,
|
|
4484
|
+
drift: maskedDrift,
|
|
3677
4485
|
errors,
|
|
3678
4486
|
warnings
|
|
3679
4487
|
};
|
|
@@ -3682,16 +4490,15 @@ function describeMatcher(matcher) {
|
|
|
3682
4490
|
if (typeof matcher === "string") return matcher;
|
|
3683
4491
|
if (matcher instanceof RegExp) return String(matcher);
|
|
3684
4492
|
if (typeof matcher === "function") {
|
|
3685
|
-
|
|
3686
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
4493
|
+
return "function (custom predicate)";
|
|
3687
4494
|
}
|
|
3688
4495
|
return String(matcher);
|
|
3689
4496
|
}
|
|
3690
4497
|
function normalizeTarget(cwd, target) {
|
|
3691
4498
|
const absolute = path10.resolve(cwd, target);
|
|
3692
|
-
const
|
|
3693
|
-
if (
|
|
3694
|
-
return
|
|
4499
|
+
const relative4 = path10.relative(cwd, absolute);
|
|
4500
|
+
if (relative4.startsWith("..") || path10.isAbsolute(relative4)) return target;
|
|
4501
|
+
return relative4.split(path10.sep).join("/");
|
|
3695
4502
|
}
|
|
3696
4503
|
function findRuleMatch(rules, relativePath) {
|
|
3697
4504
|
return rules.find(
|
|
@@ -3707,20 +4514,24 @@ function readManifest2(config) {
|
|
|
3707
4514
|
}
|
|
3708
4515
|
}
|
|
3709
4516
|
function explainPath(config, target) {
|
|
3710
|
-
const
|
|
3711
|
-
const absolute = path10.join(config.cwd,
|
|
4517
|
+
const relative4 = normalizeTarget(config.cwd, target);
|
|
4518
|
+
const absolute = path10.join(config.cwd, relative4);
|
|
4519
|
+
const maskedPath2 = findSecretInPath(relative4, config.secretRules)?.masked ?? relative4;
|
|
3712
4520
|
const result = {
|
|
3713
|
-
path:
|
|
4521
|
+
path: maskedPath2,
|
|
3714
4522
|
exists: fs9.existsSync(absolute),
|
|
3715
4523
|
included: false
|
|
3716
4524
|
};
|
|
3717
4525
|
const collection = collectFiles(config.cwd, {
|
|
3718
4526
|
includePatterns: config.include,
|
|
3719
4527
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
3720
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4528
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
4529
|
+
secretRules: config.secretRules
|
|
3721
4530
|
});
|
|
3722
|
-
if (!collection.files.includes(
|
|
3723
|
-
const record = collection.skipped.find(
|
|
4531
|
+
if (!collection.files.includes(relative4)) {
|
|
4532
|
+
const record = collection.skipped.find(
|
|
4533
|
+
(entry2) => entry2.path === relative4 || entry2.path === maskedPath2
|
|
4534
|
+
);
|
|
3724
4535
|
if (record) {
|
|
3725
4536
|
result.reason = record.reason;
|
|
3726
4537
|
result.detail = record.detail;
|
|
@@ -3734,24 +4545,30 @@ function explainPath(config, target) {
|
|
|
3734
4545
|
return result;
|
|
3735
4546
|
}
|
|
3736
4547
|
result.included = true;
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
4548
|
+
if (isBinaryFile(absolute)) {
|
|
4549
|
+
result.included = false;
|
|
4550
|
+
result.reason = "binary";
|
|
4551
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
4552
|
+
return result;
|
|
4553
|
+
}
|
|
4554
|
+
result.layer = classifyLayer(relative4, config.layers);
|
|
4555
|
+
result.kind = classifyKind(relative4, config.kinds);
|
|
4556
|
+
result.language = detectLanguage(relative4);
|
|
4557
|
+
const layerRule = findRuleMatch(config.layers, relative4);
|
|
3741
4558
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
3742
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
4559
|
+
const kindRule = findRuleMatch(config.kinds, relative4);
|
|
3743
4560
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
3744
4561
|
if (fs9.existsSync(absolute) && fs9.statSync(absolute).isFile()) {
|
|
3745
4562
|
const buffer = fs9.readFileSync(absolute);
|
|
3746
4563
|
const text2 = buffer.toString("utf8");
|
|
3747
4564
|
result.bytes = buffer.length;
|
|
3748
4565
|
result.lines = countLines(text2);
|
|
3749
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
4566
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
|
|
3750
4567
|
result.secretCount = findings.length;
|
|
3751
4568
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
3752
4569
|
}
|
|
3753
4570
|
const manifest = readManifest2(config);
|
|
3754
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
4571
|
+
const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative4) : void 0;
|
|
3755
4572
|
result.part = entry?.part ?? null;
|
|
3756
4573
|
result.inclusionMode = entry?.inclusionMode;
|
|
3757
4574
|
return result;
|
|
@@ -3806,7 +4623,40 @@ function hashMap(manifest) {
|
|
|
3806
4623
|
}
|
|
3807
4624
|
return map;
|
|
3808
4625
|
}
|
|
4626
|
+
function manifestProblem(value) {
|
|
4627
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
4628
|
+
const candidate = value;
|
|
4629
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
4630
|
+
for (const entry of candidate.files) {
|
|
4631
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4632
|
+
return 'a "files" entry is not an object';
|
|
4633
|
+
}
|
|
4634
|
+
if (typeof entry.relativePath !== "string") {
|
|
4635
|
+
return 'a "files" entry has no "relativePath"';
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
const counts = candidate.counts;
|
|
4639
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
4640
|
+
return 'no "counts" object';
|
|
4641
|
+
}
|
|
4642
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
4643
|
+
if (typeof counts[key] !== "number") {
|
|
4644
|
+
return `no numeric "counts.${key}"`;
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
4648
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
4649
|
+
return null;
|
|
4650
|
+
}
|
|
4651
|
+
function assertUsableManifest(value, label) {
|
|
4652
|
+
const problem = manifestProblem(value);
|
|
4653
|
+
if (problem) {
|
|
4654
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
3809
4657
|
function compareManifests(before, after) {
|
|
4658
|
+
assertUsableManifest(before, "before");
|
|
4659
|
+
assertUsableManifest(after, "after");
|
|
3810
4660
|
const left = hashMap(before);
|
|
3811
4661
|
const right = hashMap(after);
|
|
3812
4662
|
const added = [];
|
|
@@ -3823,12 +4673,13 @@ function compareManifests(before, after) {
|
|
|
3823
4673
|
if (!right.has(filePath)) removed.push(filePath);
|
|
3824
4674
|
}
|
|
3825
4675
|
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
4676
|
+
const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
|
|
3826
4677
|
return {
|
|
3827
4678
|
before: side(before),
|
|
3828
4679
|
after: side(after),
|
|
3829
|
-
added: sort(added),
|
|
3830
|
-
removed: sort(removed),
|
|
3831
|
-
changed: sort(changed),
|
|
4680
|
+
added: mask(sort(added)),
|
|
4681
|
+
removed: mask(sort(removed)),
|
|
4682
|
+
changed: mask(sort(changed)),
|
|
3832
4683
|
unchanged
|
|
3833
4684
|
};
|
|
3834
4685
|
}
|
|
@@ -3852,11 +4703,11 @@ function loadManifest(target) {
|
|
|
3852
4703
|
} catch (err) {
|
|
3853
4704
|
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
3854
4705
|
}
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
4706
|
+
const problem = manifestProblem(parsed);
|
|
4707
|
+
if (problem) {
|
|
4708
|
+
throw new Error(
|
|
4709
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
4710
|
+
);
|
|
3860
4711
|
}
|
|
3861
4712
|
return parsed;
|
|
3862
4713
|
}
|
|
@@ -3968,7 +4819,7 @@ function renderConfig(answers) {
|
|
|
3968
4819
|
secrets: {
|
|
3969
4820
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
3970
4821
|
customPatterns: [],
|
|
3971
|
-
// Set true to drop the
|
|
4822
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
3972
4823
|
disableDefaultPatterns: false,
|
|
3973
4824
|
},
|
|
3974
4825
|
|
|
@@ -3984,6 +4835,10 @@ function renderConfig(answers) {
|
|
|
3984
4835
|
},
|
|
3985
4836
|
// Skip the monolith above this size. null = no limit.
|
|
3986
4837
|
maxMonolithBytes: null,
|
|
4838
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
4839
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
4840
|
+
// generated file lists every setting the tool accepts.
|
|
4841
|
+
includeMonolith: ${artifacts.monolith},
|
|
3987
4842
|
|
|
3988
4843
|
// \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3989
4844
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -4305,6 +5160,14 @@ function existingConfig(cwd) {
|
|
|
4305
5160
|
}
|
|
4306
5161
|
return null;
|
|
4307
5162
|
}
|
|
5163
|
+
function packageJsonHasConfig(cwd) {
|
|
5164
|
+
try {
|
|
5165
|
+
const parsed = JSON.parse(fs9.readFileSync(path10.join(cwd, "package.json"), "utf8"));
|
|
5166
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
5167
|
+
} catch {
|
|
5168
|
+
return false;
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
4308
5171
|
function artifactAnswers(options) {
|
|
4309
5172
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
4310
5173
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -4517,6 +5380,11 @@ async function runInit(options) {
|
|
|
4517
5380
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
4518
5381
|
);
|
|
4519
5382
|
}
|
|
5383
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
5384
|
+
throw new Error(
|
|
5385
|
+
'package.json already defines a "snapshot" config. Remove that key, or re-run with --force to write a config file that takes precedence over it.'
|
|
5386
|
+
);
|
|
5387
|
+
}
|
|
4520
5388
|
if (fs9.existsSync(configPath) && !options.force) {
|
|
4521
5389
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
4522
5390
|
}
|
|
@@ -4536,7 +5404,7 @@ async function runInit(options) {
|
|
|
4536
5404
|
|
|
4537
5405
|
// src/index.ts
|
|
4538
5406
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4539
|
-
var GENERATOR_VERSION = "
|
|
5407
|
+
var GENERATOR_VERSION = "4.0.1";
|
|
4540
5408
|
var SCHEMA_VERSION = "3.0.0";
|
|
4541
5409
|
function sha256Hex2(input) {
|
|
4542
5410
|
return crypto.createHash("sha256").update(input).digest("hex");
|
|
@@ -4573,7 +5441,6 @@ function bump(counts, key) {
|
|
|
4573
5441
|
}
|
|
4574
5442
|
var RELATIVE_SPECIFIER = /^\.{1,2}\//;
|
|
4575
5443
|
var IMPORT_CANDIDATE_SUFFIXES = [
|
|
4576
|
-
"",
|
|
4577
5444
|
".ts",
|
|
4578
5445
|
".tsx",
|
|
4579
5446
|
".mts",
|
|
@@ -4587,23 +5454,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
|
|
|
4587
5454
|
"/index.js",
|
|
4588
5455
|
"/index.mjs"
|
|
4589
5456
|
];
|
|
4590
|
-
|
|
5457
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5458
|
+
".ts",
|
|
5459
|
+
".tsx",
|
|
5460
|
+
".mts",
|
|
5461
|
+
".cts",
|
|
5462
|
+
".js",
|
|
5463
|
+
".jsx",
|
|
5464
|
+
".mjs",
|
|
5465
|
+
".cjs"
|
|
5466
|
+
]);
|
|
5467
|
+
function buildImportResolver(files) {
|
|
5468
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5469
|
+
const byPathFolded = /* @__PURE__ */ new Map();
|
|
5470
|
+
const byStem = /* @__PURE__ */ new Map();
|
|
5471
|
+
const byStemFolded = /* @__PURE__ */ new Map();
|
|
5472
|
+
const remember = (map, key, value) => {
|
|
5473
|
+
if (!map.has(key)) map.set(key, value);
|
|
5474
|
+
};
|
|
5475
|
+
for (const file of files) {
|
|
5476
|
+
const relativePath = file.relativePath;
|
|
5477
|
+
remember(byPath, relativePath, relativePath);
|
|
5478
|
+
remember(byPathFolded, relativePath.toLowerCase(), relativePath);
|
|
5479
|
+
const extension = path10.posix.extname(relativePath);
|
|
5480
|
+
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
|
5481
|
+
const stem = relativePath.slice(0, -extension.length);
|
|
5482
|
+
remember(byStem, stem, relativePath);
|
|
5483
|
+
remember(byStemFolded, stem.toLowerCase(), relativePath);
|
|
5484
|
+
if (path10.posix.basename(stem) === "index") {
|
|
5485
|
+
const directory = path10.posix.dirname(stem);
|
|
5486
|
+
remember(byStem, directory, relativePath);
|
|
5487
|
+
remember(byStemFolded, directory.toLowerCase(), relativePath);
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
return { byPath, byPathFolded, byStem, byStemFolded };
|
|
5491
|
+
}
|
|
5492
|
+
function resolveSpecifier(fromFile, specifier, resolver) {
|
|
4591
5493
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
-
|
|
5494
|
+
const joined = path10.posix.normalize(
|
|
5495
|
+
path10.posix.join(path10.posix.dirname(fromFile), specifier)
|
|
5496
|
+
);
|
|
5497
|
+
const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
|
|
5498
|
+
const direct = exact(joined);
|
|
5499
|
+
if (direct) return direct;
|
|
4595
5500
|
for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
|
|
4596
|
-
const
|
|
4597
|
-
if (
|
|
5501
|
+
const hit = exact(`${joined}${suffix}`);
|
|
5502
|
+
if (hit) return hit;
|
|
4598
5503
|
}
|
|
4599
|
-
const
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
}
|
|
4603
|
-
return null;
|
|
5504
|
+
const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
|
|
5505
|
+
const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
|
|
5506
|
+
return byStem(joined) ?? byStem(stem);
|
|
4604
5507
|
}
|
|
4605
5508
|
function buildIndexes(files) {
|
|
4606
|
-
const
|
|
5509
|
+
const resolver = buildImportResolver(files);
|
|
4607
5510
|
const importerCounts = /* @__PURE__ */ new Map();
|
|
4608
5511
|
const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
|
|
4609
5512
|
const summary = file.summary;
|
|
@@ -4615,7 +5518,7 @@ function buildIndexes(files) {
|
|
|
4615
5518
|
});
|
|
4616
5519
|
for (const entry of byFile) {
|
|
4617
5520
|
for (const specifier of entry.imports) {
|
|
4618
|
-
const resolved = resolveSpecifier(entry.path, specifier,
|
|
5521
|
+
const resolved = resolveSpecifier(entry.path, specifier, resolver);
|
|
4619
5522
|
if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
|
|
4620
5523
|
}
|
|
4621
5524
|
}
|
|
@@ -4706,29 +5609,224 @@ function resolveSafeOutputDirs(config) {
|
|
|
4706
5609
|
return { outDir, tmpDir };
|
|
4707
5610
|
}
|
|
4708
5611
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5612
|
+
var RUN_DIR_PREFIX = "run-";
|
|
5613
|
+
var STALE_RUN_MS = 60 * 60 * 1e3;
|
|
5614
|
+
function maskPathText(value, secretRules) {
|
|
5615
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
5616
|
+
}
|
|
5617
|
+
function maskSkipRecords(records, secretRules) {
|
|
5618
|
+
return maskSkipPaths(records, secretRules);
|
|
5619
|
+
}
|
|
5620
|
+
function isProcessAlive(pid) {
|
|
5621
|
+
try {
|
|
5622
|
+
process.kill(pid, 0);
|
|
5623
|
+
return true;
|
|
5624
|
+
} catch (err) {
|
|
5625
|
+
return err.code !== "ESRCH";
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
4709
5628
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4710
|
-
|
|
5629
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5630
|
+
var SNAPSHOT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5631
|
+
function listEntries(dir) {
|
|
5632
|
+
const entries = [];
|
|
5633
|
+
const walk = (absolute, prefix) => {
|
|
5634
|
+
let children;
|
|
5635
|
+
try {
|
|
5636
|
+
children = fs9.readdirSync(absolute, { withFileTypes: true });
|
|
5637
|
+
} catch {
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
for (const child of children) {
|
|
5641
|
+
const relative4 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5642
|
+
entries.push(relative4);
|
|
5643
|
+
if (child.isDirectory()) walk(path10.join(absolute, child.name), relative4);
|
|
5644
|
+
}
|
|
5645
|
+
};
|
|
5646
|
+
walk(dir, "");
|
|
5647
|
+
return entries.sort();
|
|
5648
|
+
}
|
|
5649
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4711
5650
|
const marker2 = {
|
|
4712
5651
|
generator: GENERATOR_NAME,
|
|
4713
5652
|
version: GENERATOR_VERSION,
|
|
4714
|
-
schemaVersion: SCHEMA_VERSION
|
|
5653
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5654
|
+
createdAt,
|
|
5655
|
+
state,
|
|
5656
|
+
id,
|
|
5657
|
+
pid: process.pid,
|
|
5658
|
+
entries: listEntries(dir)
|
|
4715
5659
|
};
|
|
4716
|
-
fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
|
|
4717
|
-
`, "utf8");
|
|
4718
|
-
}
|
|
4719
|
-
function isOwnOutput(dir) {
|
|
4720
|
-
if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
|
|
4721
5660
|
const manifestPath = path10.join(dir, "manifest.json");
|
|
4722
|
-
if (
|
|
5661
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5662
|
+
marker2.manifestSha256 = sha256Hex2(fs9.readFileSync(manifestPath));
|
|
5663
|
+
}
|
|
5664
|
+
return marker2;
|
|
5665
|
+
}
|
|
5666
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5667
|
+
fs9.writeFileSync(
|
|
5668
|
+
path10.join(dir, OWNERSHIP_MARKER),
|
|
5669
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5670
|
+
`,
|
|
5671
|
+
"utf8"
|
|
5672
|
+
);
|
|
5673
|
+
}
|
|
5674
|
+
function readOwnershipMarker(dir) {
|
|
5675
|
+
const markerPath = path10.join(dir, OWNERSHIP_MARKER);
|
|
5676
|
+
if (!fs9.existsSync(markerPath)) {
|
|
5677
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5678
|
+
}
|
|
5679
|
+
let raw;
|
|
4723
5680
|
try {
|
|
4724
|
-
|
|
4725
|
-
const generator = parsed.generator;
|
|
4726
|
-
return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
|
|
5681
|
+
raw = fs9.readFileSync(markerPath, "utf8");
|
|
4727
5682
|
} catch {
|
|
4728
|
-
return
|
|
5683
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5684
|
+
}
|
|
5685
|
+
if (raw.trim().length === 0) {
|
|
5686
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5687
|
+
}
|
|
5688
|
+
let parsed;
|
|
5689
|
+
try {
|
|
5690
|
+
parsed = JSON.parse(raw);
|
|
5691
|
+
} catch {
|
|
5692
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5693
|
+
}
|
|
5694
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5695
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5696
|
+
}
|
|
5697
|
+
const candidate = parsed;
|
|
5698
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5699
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5700
|
+
}
|
|
5701
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5702
|
+
return {
|
|
5703
|
+
marker: null,
|
|
5704
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5708
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5709
|
+
}
|
|
5710
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5711
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5712
|
+
}
|
|
5713
|
+
return {
|
|
5714
|
+
marker: {
|
|
5715
|
+
generator: GENERATOR_NAME,
|
|
5716
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5717
|
+
schemaVersion: candidate.schemaVersion,
|
|
5718
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5719
|
+
state: candidate.state,
|
|
5720
|
+
id: candidate.id,
|
|
5721
|
+
pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
|
|
5722
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5723
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5724
|
+
},
|
|
5725
|
+
problem: null
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5728
|
+
function describeManifestProblem(value) {
|
|
5729
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5730
|
+
const candidate = value;
|
|
5731
|
+
const generator = candidate.generator;
|
|
5732
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5733
|
+
return "was not written by this generator";
|
|
5734
|
+
}
|
|
5735
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5736
|
+
return null;
|
|
5737
|
+
}
|
|
5738
|
+
function assessSnapshotDirectory(dir) {
|
|
5739
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5740
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5741
|
+
if (marker2.state !== "complete") {
|
|
5742
|
+
return {
|
|
5743
|
+
owned: false,
|
|
5744
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5745
|
+
foreignEntries: [],
|
|
5746
|
+
marker: marker2
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
const manifestPath = path10.join(dir, "manifest.json");
|
|
5750
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5751
|
+
let raw;
|
|
5752
|
+
try {
|
|
5753
|
+
raw = fs9.readFileSync(manifestPath, "utf8");
|
|
5754
|
+
} catch {
|
|
5755
|
+
return {
|
|
5756
|
+
owned: false,
|
|
5757
|
+
problem: "its manifest.json could not be read",
|
|
5758
|
+
foreignEntries: [],
|
|
5759
|
+
marker: marker2
|
|
5760
|
+
};
|
|
5761
|
+
}
|
|
5762
|
+
let parsed;
|
|
5763
|
+
try {
|
|
5764
|
+
parsed = JSON.parse(raw);
|
|
5765
|
+
} catch {
|
|
5766
|
+
return {
|
|
5767
|
+
owned: false,
|
|
5768
|
+
problem: "its manifest.json is not valid JSON",
|
|
5769
|
+
foreignEntries: [],
|
|
5770
|
+
marker: marker2
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5774
|
+
if (manifestProblem2) {
|
|
5775
|
+
return {
|
|
5776
|
+
owned: false,
|
|
5777
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5778
|
+
foreignEntries: [],
|
|
5779
|
+
marker: marker2
|
|
5780
|
+
};
|
|
5781
|
+
}
|
|
5782
|
+
if (!marker2.manifestSha256) {
|
|
5783
|
+
return {
|
|
5784
|
+
owned: false,
|
|
5785
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5786
|
+
foreignEntries: [],
|
|
5787
|
+
marker: marker2
|
|
5788
|
+
};
|
|
5789
|
+
}
|
|
5790
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5791
|
+
return {
|
|
5792
|
+
owned: false,
|
|
5793
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5794
|
+
foreignEntries: [],
|
|
5795
|
+
marker: marker2
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
} else if (marker2.manifestSha256) {
|
|
5799
|
+
return {
|
|
5800
|
+
owned: false,
|
|
5801
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5802
|
+
foreignEntries: [],
|
|
5803
|
+
marker: marker2
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5807
|
+
if (recorded.size === 0) {
|
|
5808
|
+
return {
|
|
5809
|
+
owned: false,
|
|
5810
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5811
|
+
foreignEntries: [],
|
|
5812
|
+
marker: marker2
|
|
5813
|
+
};
|
|
4729
5814
|
}
|
|
5815
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5816
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5817
|
+
);
|
|
5818
|
+
return {
|
|
5819
|
+
owned: foreignEntries.length === 0,
|
|
5820
|
+
problem: null,
|
|
5821
|
+
foreignEntries,
|
|
5822
|
+
marker: marker2
|
|
5823
|
+
};
|
|
5824
|
+
}
|
|
5825
|
+
function describeEntries(entries, limit = 5) {
|
|
5826
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5827
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4730
5828
|
}
|
|
4731
|
-
function assertReplaceable(target, label) {
|
|
5829
|
+
function assertReplaceable(target, label, secretRules) {
|
|
4732
5830
|
if (!fs9.existsSync(target)) return;
|
|
4733
5831
|
if (!fs9.statSync(target).isDirectory()) {
|
|
4734
5832
|
throw new Error(
|
|
@@ -4736,26 +5834,210 @@ function assertReplaceable(target, label) {
|
|
|
4736
5834
|
);
|
|
4737
5835
|
}
|
|
4738
5836
|
const entries = fs9.readdirSync(target);
|
|
4739
|
-
|
|
4740
|
-
if (
|
|
4741
|
-
|
|
4742
|
-
const
|
|
4743
|
-
|
|
5837
|
+
if (entries.length === 0) return;
|
|
5838
|
+
if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5839
|
+
const { problem } = readOwnershipMarker(target);
|
|
5840
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5841
|
+
if (verdict.owned) return;
|
|
5842
|
+
const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
|
|
5843
|
+
const shownEntries = entries.map((name) => maskPathText(name, secretRules));
|
|
5844
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5845
|
+
throw new Error(
|
|
5846
|
+
`Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(shownForeign)}. Remove them yourself, or point ${label} at a new or empty directory.`
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5849
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4744
5850
|
throw new Error(
|
|
4745
|
-
`Refusing to replace ${label} ${target}:
|
|
5851
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
4746
5852
|
);
|
|
4747
5853
|
}
|
|
4748
|
-
function
|
|
5854
|
+
function assertStagingRootUsable(tmpDir, secretRules) {
|
|
5855
|
+
if (!fs9.existsSync(tmpDir)) return;
|
|
5856
|
+
if (!fs9.statSync(tmpDir).isDirectory()) {
|
|
5857
|
+
throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
|
|
5858
|
+
}
|
|
5859
|
+
const entries = fs9.readdirSync(tmpDir);
|
|
5860
|
+
if (entries.length === 0) return;
|
|
5861
|
+
const foreign = entries.filter((name) => {
|
|
5862
|
+
const full = path10.join(tmpDir, name);
|
|
5863
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) return true;
|
|
5864
|
+
let stats;
|
|
5865
|
+
try {
|
|
5866
|
+
stats = fs9.statSync(full);
|
|
5867
|
+
} catch {
|
|
5868
|
+
return true;
|
|
5869
|
+
}
|
|
5870
|
+
if (!stats.isDirectory()) return true;
|
|
5871
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5872
|
+
if (marker2) return false;
|
|
5873
|
+
try {
|
|
5874
|
+
return fs9.readdirSync(full).length > 0;
|
|
5875
|
+
} catch {
|
|
5876
|
+
return true;
|
|
5877
|
+
}
|
|
5878
|
+
});
|
|
5879
|
+
if (foreign.length > 0) {
|
|
5880
|
+
const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
|
|
5881
|
+
throw new Error(
|
|
5882
|
+
`Refusing to replace tmpDir ${tmpDir}: it holds ${shown}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
|
|
5883
|
+
);
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
function cleanStaleRunDirs(tmpDir) {
|
|
5887
|
+
let entries;
|
|
5888
|
+
try {
|
|
5889
|
+
entries = fs9.readdirSync(tmpDir);
|
|
5890
|
+
} catch {
|
|
5891
|
+
return;
|
|
5892
|
+
}
|
|
5893
|
+
for (const name of entries) {
|
|
5894
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) continue;
|
|
5895
|
+
const full = path10.join(tmpDir, name);
|
|
5896
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5897
|
+
if (marker2) {
|
|
5898
|
+
const created = Date.parse(marker2.createdAt);
|
|
5899
|
+
const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
|
|
5900
|
+
const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
|
|
5901
|
+
if (aged || abandoned) {
|
|
5902
|
+
try {
|
|
5903
|
+
fs9.rmSync(full, { recursive: true, force: true });
|
|
5904
|
+
} catch {
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
continue;
|
|
5908
|
+
}
|
|
5909
|
+
try {
|
|
5910
|
+
if (fs9.readdirSync(full).length === 0) fs9.rmSync(full, { recursive: true, force: true });
|
|
5911
|
+
} catch {
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
function removeIfEmpty(dir) {
|
|
5916
|
+
try {
|
|
5917
|
+
if (fs9.readdirSync(dir).length === 0) fs9.rmSync(dir, { recursive: true, force: true });
|
|
5918
|
+
} catch {
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
function ensureParentDirectory(target, label) {
|
|
5922
|
+
const parent = path10.dirname(target);
|
|
5923
|
+
if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
|
|
5924
|
+
throw new Error(
|
|
5925
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5926
|
+
);
|
|
5927
|
+
}
|
|
5928
|
+
try {
|
|
5929
|
+
fs9.mkdirSync(parent, { recursive: true });
|
|
5930
|
+
} catch (err) {
|
|
5931
|
+
throw new Error(
|
|
5932
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5933
|
+
);
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
function acquireRunLock(stagingRoot) {
|
|
5937
|
+
const key = lockKeyFor(stagingRoot);
|
|
5938
|
+
const lockPath = path10.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
|
|
5939
|
+
const payload = `${JSON.stringify({
|
|
5940
|
+
pid: process.pid,
|
|
5941
|
+
stagingRoot,
|
|
5942
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5943
|
+
})}
|
|
5944
|
+
`;
|
|
5945
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5946
|
+
try {
|
|
5947
|
+
const handle = fs9.openSync(lockPath, "wx");
|
|
5948
|
+
try {
|
|
5949
|
+
fs9.writeSync(handle, payload);
|
|
5950
|
+
} finally {
|
|
5951
|
+
fs9.closeSync(handle);
|
|
5952
|
+
}
|
|
5953
|
+
return () => {
|
|
5954
|
+
try {
|
|
5955
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5956
|
+
} catch {
|
|
5957
|
+
}
|
|
5958
|
+
};
|
|
5959
|
+
} catch (err) {
|
|
5960
|
+
if (err.code !== "EEXIST") throw err;
|
|
5961
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5962
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
throw new Error(
|
|
5966
|
+
`Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
|
|
5971
|
+
}
|
|
5972
|
+
function lockKeyFor(stagingRoot) {
|
|
5973
|
+
let key = stagingRoot;
|
|
5974
|
+
try {
|
|
5975
|
+
key = fs9.realpathSync(stagingRoot);
|
|
5976
|
+
} catch {
|
|
5977
|
+
}
|
|
5978
|
+
if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
|
|
5979
|
+
return sha256Hex2(key).slice(0, 16);
|
|
5980
|
+
}
|
|
5981
|
+
function lockOwnerIsGone(lockPath) {
|
|
5982
|
+
let raw;
|
|
5983
|
+
try {
|
|
5984
|
+
raw = fs9.readFileSync(lockPath, "utf8");
|
|
5985
|
+
} catch {
|
|
5986
|
+
return false;
|
|
5987
|
+
}
|
|
5988
|
+
let pid;
|
|
5989
|
+
try {
|
|
5990
|
+
pid = JSON.parse(raw).pid;
|
|
5991
|
+
} catch {
|
|
5992
|
+
return false;
|
|
5993
|
+
}
|
|
5994
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
5995
|
+
try {
|
|
5996
|
+
process.kill(pid, 0);
|
|
5997
|
+
return false;
|
|
5998
|
+
} catch (err) {
|
|
5999
|
+
return err.code === "ESRCH";
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
6003
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
6004
|
+
fs9.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
6005
|
+
try {
|
|
6006
|
+
fs9.renameSync(tempPath, manifestPath);
|
|
6007
|
+
} catch (err) {
|
|
6008
|
+
try {
|
|
6009
|
+
fs9.rmSync(tempPath, { force: true });
|
|
6010
|
+
} catch {
|
|
6011
|
+
}
|
|
6012
|
+
throw err;
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
function swapIntoPlace(stagingDir, outDir) {
|
|
6016
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6017
|
+
const deadline = Date.now() + 3e3;
|
|
6018
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
6019
|
+
try {
|
|
6020
|
+
swapOnce(stagingDir, outDir);
|
|
6021
|
+
return;
|
|
6022
|
+
} catch (err) {
|
|
6023
|
+
const code = err.code;
|
|
6024
|
+
const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
|
|
6025
|
+
if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
|
|
6026
|
+
sleepSync(50);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
6030
|
+
function swapOnce(stagingDir, outDir) {
|
|
4749
6031
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4750
6032
|
const hadPrevious = fs9.existsSync(outDir);
|
|
4751
6033
|
if (hadPrevious) fs9.renameSync(outDir, backupDir);
|
|
4752
6034
|
try {
|
|
4753
6035
|
try {
|
|
4754
|
-
fs9.renameSync(
|
|
6036
|
+
fs9.renameSync(stagingDir, outDir);
|
|
4755
6037
|
} catch (err) {
|
|
4756
6038
|
if (err.code === "EXDEV") {
|
|
4757
|
-
fs9.cpSync(
|
|
4758
|
-
fs9.rmSync(
|
|
6039
|
+
fs9.cpSync(stagingDir, outDir, { recursive: true });
|
|
6040
|
+
fs9.rmSync(stagingDir, { recursive: true, force: true });
|
|
4759
6041
|
} else {
|
|
4760
6042
|
throw err;
|
|
4761
6043
|
}
|
|
@@ -4769,6 +6051,10 @@ function swapIntoPlace(tmpDir, outDir) {
|
|
|
4769
6051
|
throw err;
|
|
4770
6052
|
}
|
|
4771
6053
|
}
|
|
6054
|
+
function sleepSync(milliseconds) {
|
|
6055
|
+
const buffer = new Int32Array(new SharedArrayBuffer(4));
|
|
6056
|
+
Atomics.wait(buffer, 0, 0, milliseconds);
|
|
6057
|
+
}
|
|
4772
6058
|
function artifactWarnings(artifacts) {
|
|
4773
6059
|
const messages = [];
|
|
4774
6060
|
if (!artifacts.manifest) {
|
|
@@ -4831,10 +6117,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4831
6117
|
const collection = collectFiles(config.cwd, {
|
|
4832
6118
|
includePatterns: config.include,
|
|
4833
6119
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
4834
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
6120
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
6121
|
+
secretRules: config.secretRules
|
|
4835
6122
|
});
|
|
4836
6123
|
const files = [];
|
|
4837
|
-
const
|
|
6124
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
6125
|
+
const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
|
|
4838
6126
|
const secretFindings = [];
|
|
4839
6127
|
const warnings = [];
|
|
4840
6128
|
const layerCounts = {};
|
|
@@ -4897,6 +6185,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4897
6185
|
isPreviewTruncated = preview.isTruncated;
|
|
4898
6186
|
includedLines = preview.includedLines;
|
|
4899
6187
|
}
|
|
6188
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6189
|
+
monolithBodies.set(relativePath, redacted);
|
|
6190
|
+
}
|
|
4900
6191
|
let summary;
|
|
4901
6192
|
let summaryQuality;
|
|
4902
6193
|
try {
|
|
@@ -4905,12 +6196,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4905
6196
|
summaryQuality = outcome?.quality;
|
|
4906
6197
|
if (isSourceSummary(summary) && summary.parseErrors) {
|
|
4907
6198
|
warnings.push(
|
|
4908
|
-
`Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
|
|
6199
|
+
`Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
|
|
4909
6200
|
);
|
|
4910
6201
|
}
|
|
4911
6202
|
} catch (err) {
|
|
4912
6203
|
warnings.push(
|
|
4913
|
-
`Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
|
|
6204
|
+
`Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
|
|
4914
6205
|
);
|
|
4915
6206
|
}
|
|
4916
6207
|
files.push({
|
|
@@ -4939,7 +6230,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4939
6230
|
).join("\n")
|
|
4940
6231
|
);
|
|
4941
6232
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4942
|
-
for (const file of files)
|
|
6233
|
+
for (const file of files) {
|
|
6234
|
+
fullContents.set(
|
|
6235
|
+
file.relativePath,
|
|
6236
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6237
|
+
);
|
|
6238
|
+
}
|
|
4943
6239
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4944
6240
|
const maxEffectiveBudget = Math.max(
|
|
4945
6241
|
config.maxPartBytes,
|
|
@@ -5014,21 +6310,28 @@ function generateSnapshot(configInput, options) {
|
|
|
5014
6310
|
const { content: _content, ...rest } = file;
|
|
5015
6311
|
return rest;
|
|
5016
6312
|
}),
|
|
5017
|
-
skipped,
|
|
6313
|
+
skipped: maskSkipRecords(skipped, config.secretRules),
|
|
5018
6314
|
warnings
|
|
5019
6315
|
};
|
|
5020
6316
|
manifest.warnings = [
|
|
5021
6317
|
...warnings,
|
|
5022
|
-
...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
|
|
6318
|
+
...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
|
|
5023
6319
|
];
|
|
5024
6320
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
5025
6321
|
let monolithJson = null;
|
|
5026
6322
|
let monolithMarkdown = null;
|
|
5027
6323
|
if (config.includeMonolith) {
|
|
5028
|
-
const fullFiles = files.map((file) =>
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
6324
|
+
const fullFiles = files.map((file) => {
|
|
6325
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6326
|
+
return {
|
|
6327
|
+
...file,
|
|
6328
|
+
content,
|
|
6329
|
+
inclusionMode: "full",
|
|
6330
|
+
isPreviewTruncated: false,
|
|
6331
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6332
|
+
includedLines: countLines(content)
|
|
6333
|
+
};
|
|
6334
|
+
});
|
|
5032
6335
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5033
6336
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5034
6337
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5056,62 +6359,97 @@ function generateSnapshot(configInput, options) {
|
|
|
5056
6359
|
}
|
|
5057
6360
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5058
6361
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6362
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6363
|
+
const { artifacts } = config;
|
|
6364
|
+
assertReplaceable(outDir, "outDir", config.secretRules);
|
|
6365
|
+
assertStagingRootUsable(tmpDir, config.secretRules);
|
|
5059
6366
|
if (dryRun) {
|
|
5060
6367
|
runAfterGenerate(config, manifest);
|
|
5061
6368
|
return manifest;
|
|
5062
6369
|
}
|
|
5063
|
-
const
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
6370
|
+
const releaseLock = acquireRunLock(tmpDir);
|
|
6371
|
+
let stagingDir = null;
|
|
6372
|
+
try {
|
|
6373
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6374
|
+
fs9.mkdirSync(tmpDir, { recursive: true });
|
|
6375
|
+
cleanStaleRunDirs(tmpDir);
|
|
6376
|
+
const snapshotId = crypto.randomUUID();
|
|
6377
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6378
|
+
stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
6379
|
+
fs9.mkdirSync(stagingDir);
|
|
6380
|
+
writeOwnershipMarker(stagingDir, {
|
|
6381
|
+
generator: GENERATOR_NAME,
|
|
6382
|
+
version: GENERATOR_VERSION,
|
|
6383
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6384
|
+
createdAt,
|
|
6385
|
+
state: "staging",
|
|
6386
|
+
id: snapshotId,
|
|
6387
|
+
pid: process.pid
|
|
6388
|
+
});
|
|
6389
|
+
const partsDir = path10.join(stagingDir, "parts");
|
|
6390
|
+
const reportsDir = path10.join(stagingDir, "reports");
|
|
6391
|
+
if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
|
|
6392
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6393
|
+
fs9.mkdirSync(reportsDir, { recursive: true });
|
|
6394
|
+
}
|
|
6395
|
+
if (artifacts.parts) {
|
|
6396
|
+
for (const part of parts) {
|
|
6397
|
+
fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6398
|
+
}
|
|
5079
6399
|
}
|
|
6400
|
+
if (artifacts.projectTree) {
|
|
6401
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6402
|
+
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6403
|
+
}
|
|
6404
|
+
if (artifacts.navigation) {
|
|
6405
|
+
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6406
|
+
}
|
|
6407
|
+
if (artifacts.audit) {
|
|
6408
|
+
fs9.writeFileSync(
|
|
6409
|
+
path10.join(reportsDir, "audit.md"),
|
|
6410
|
+
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
6411
|
+
"utf8"
|
|
6412
|
+
);
|
|
6413
|
+
}
|
|
6414
|
+
if (artifacts.readme) {
|
|
6415
|
+
fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6416
|
+
}
|
|
6417
|
+
if (artifacts.manifest) {
|
|
6418
|
+
fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6419
|
+
}
|
|
6420
|
+
if (monolithJson !== null) {
|
|
6421
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6422
|
+
}
|
|
6423
|
+
if (monolithMarkdown !== null) {
|
|
6424
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6425
|
+
}
|
|
6426
|
+
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
6427
|
+
swapIntoPlace(stagingDir, outDir);
|
|
6428
|
+
stagingDir = null;
|
|
6429
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6430
|
+
runAfterGenerate(config, manifest);
|
|
6431
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6432
|
+
writeManifestAtomically(path10.join(outDir, "manifest.json"), manifest);
|
|
6433
|
+
}
|
|
6434
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6435
|
+
return manifest;
|
|
6436
|
+
} catch (err) {
|
|
6437
|
+
if (stagingDir !== null) {
|
|
6438
|
+
try {
|
|
6439
|
+
fs9.rmSync(stagingDir, { recursive: true, force: true });
|
|
6440
|
+
} catch {
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
throw err;
|
|
6444
|
+
} finally {
|
|
6445
|
+
removeIfEmpty(tmpDir);
|
|
6446
|
+
releaseLock();
|
|
5080
6447
|
}
|
|
5081
|
-
if (artifacts.projectTree) {
|
|
5082
|
-
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
5083
|
-
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
5084
|
-
}
|
|
5085
|
-
if (artifacts.navigation) {
|
|
5086
|
-
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
5087
|
-
}
|
|
5088
|
-
if (artifacts.audit) {
|
|
5089
|
-
fs9.writeFileSync(
|
|
5090
|
-
path10.join(reportsDir, "audit.md"),
|
|
5091
|
-
generateAuditReport(manifest, secretFindings),
|
|
5092
|
-
"utf8"
|
|
5093
|
-
);
|
|
5094
|
-
}
|
|
5095
|
-
if (artifacts.readme) {
|
|
5096
|
-
fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
5097
|
-
}
|
|
5098
|
-
if (artifacts.manifest) {
|
|
5099
|
-
fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
5100
|
-
}
|
|
5101
|
-
if (monolithJson !== null) {
|
|
5102
|
-
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
5103
|
-
}
|
|
5104
|
-
if (monolithMarkdown !== null) {
|
|
5105
|
-
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
5106
|
-
}
|
|
5107
|
-
swapIntoPlace(tmpDir, outDir);
|
|
5108
|
-
runAfterGenerate(config, manifest);
|
|
5109
|
-
return manifest;
|
|
5110
6448
|
}
|
|
5111
6449
|
async function generateSnapshotAsync(configInput, options) {
|
|
5112
6450
|
return generateSnapshot(configInput, options);
|
|
5113
6451
|
}
|
|
5114
6452
|
|
|
5115
|
-
export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
|
|
5116
|
-
//# sourceMappingURL=chunk-
|
|
5117
|
-
//# sourceMappingURL=chunk-
|
|
6453
|
+
export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, fenceForText, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
|
|
6454
|
+
//# sourceMappingURL=chunk-RGIMMEAW.js.map
|
|
6455
|
+
//# sourceMappingURL=chunk-RGIMMEAW.js.map
|