code-auditor-mcp 3.6.0 → 3.8.0
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +73 -0
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +12 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +122 -43
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +6 -7
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +152 -20
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +38 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +437 -48
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +6 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +117 -35
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +86 -15
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +32 -7
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +104 -23
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts +55 -0
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +318 -0
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -0
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -8
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +54 -45
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/functionConcerns.d.ts +57 -0
- package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -0
- package/dist/analyzers/universal/functionConcerns.js +239 -0
- package/dist/analyzers/universal/functionConcerns.js.map +1 -0
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +13 -7
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +124 -11
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/auditRouter.d.ts.map +1 -1
- package/dist/auditRouter.js +4 -6
- package/dist/auditRouter.js.map +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +74 -48
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +321 -98
- package/dist/cli.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +23 -0
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +45 -10
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +2 -0
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/conventions/conventionMiner.d.ts +18 -3
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +130 -12
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/daemon/core.d.ts +128 -0
- package/dist/daemon/core.d.ts.map +1 -0
- package/dist/daemon/core.js +700 -0
- package/dist/daemon/core.js.map +1 -0
- package/dist/daemon/lspServer.d.ts +51 -0
- package/dist/daemon/lspServer.d.ts.map +1 -0
- package/dist/daemon/lspServer.js +168 -0
- package/dist/daemon/lspServer.js.map +1 -0
- package/dist/daemon/main.d.ts +34 -0
- package/dist/daemon/main.d.ts.map +1 -0
- package/dist/daemon/main.js +147 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/resolve.d.ts +41 -0
- package/dist/daemon/resolve.d.ts.map +1 -0
- package/dist/daemon/resolve.js +90 -0
- package/dist/daemon/resolve.js.map +1 -0
- package/dist/daemon/socketClient.d.ts +21 -0
- package/dist/daemon/socketClient.d.ts.map +1 -0
- package/dist/daemon/socketClient.js +60 -0
- package/dist/daemon/socketClient.js.map +1 -0
- package/dist/daemon/socketServer.d.ts +33 -0
- package/dist/daemon/socketServer.d.ts.map +1 -0
- package/dist/daemon/socketServer.js +106 -0
- package/dist/daemon/socketServer.js.map +1 -0
- package/dist/daemon/types.d.ts +78 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +10 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/dataPaths.d.ts +21 -0
- package/dist/dataPaths.d.ts.map +1 -1
- package/dist/dataPaths.js +27 -0
- package/dist/dataPaths.js.map +1 -1
- package/dist/graph/importGraph.d.ts +11 -0
- package/dist/graph/importGraph.d.ts.map +1 -1
- package/dist/graph/importGraph.js +11 -3
- package/dist/graph/importGraph.js.map +1 -1
- package/dist/hooks/core.d.ts.map +1 -1
- package/dist/hooks/core.js +21 -4
- package/dist/hooks/core.js.map +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +2 -5
- package/dist/installer.js.map +1 -1
- package/dist/languages/RuntimeManager.js +10 -0
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/go/analyzer +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +166 -35
- package/dist/languages/go/analyzer-src/solid.go +80 -172
- package/dist/languages/go/analyzer-src/types.go +18 -18
- package/dist/ledger.d.ts +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +3 -3
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.js +1 -1
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/nextFileIncremental.d.ts +9 -3
- package/dist/nextFileIncremental.d.ts.map +1 -1
- package/dist/nextFileIncremental.js +32 -6
- package/dist/nextFileIncremental.js.map +1 -1
- package/dist/pipeline.d.ts +26 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +86 -10
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +2 -1
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +228 -5
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/csvReportGenerator.js +1 -1
- package/dist/ruleAliases.d.ts.map +1 -1
- package/dist/ruleAliases.js +41 -2
- package/dist/ruleAliases.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +8 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +11 -1
- package/dist/styles/tailwindProbe.js.map +1 -1
- package/dist/types/crossLanguage.d.ts +1 -0
- package/dist/types/crossLanguage.d.ts.map +1 -1
- package/dist/types.d.ts +30 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +4 -2
- package/plugin/skills/code-auditor/SKILL.md +19 -11
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Secrets Analyzer
|
|
3
|
+
* Detects hardcoded credentials, API keys, and tokens in source.
|
|
4
|
+
*
|
|
5
|
+
* The reference case is a Playwright/Puppeteer automation script typing a real
|
|
6
|
+
* password into a login form:
|
|
7
|
+
*
|
|
8
|
+
* page.type('#password', 'vyy8AUVvish34Fq')
|
|
9
|
+
*
|
|
10
|
+
* A secret is a string literal that (a) sits in a credential position — a
|
|
11
|
+
* variable/field/object-key named like a secret, or a call argument whose
|
|
12
|
+
* sibling argument is a credential selector (`#password`) — and (b) looks like
|
|
13
|
+
* a real value rather than a placeholder (`'password'`, `'your-api-key'`,
|
|
14
|
+
* `'changeme'`).
|
|
15
|
+
*
|
|
16
|
+
* Near-miss classes that must stay silent (critical severity gates — a false
|
|
17
|
+
* positive blocks the edit loop, so precision is the design constraint):
|
|
18
|
+
* - test fixtures / mocks (`.test.*`, `.spec.*`, `__tests__`, `fixtures`) —
|
|
19
|
+
* excluded by file path, before any literal is read;
|
|
20
|
+
* - placeholder values (`'changeme'`, `'your-api-key'`, `'<token>'`) —
|
|
21
|
+
* rejected by the value heuristic;
|
|
22
|
+
* - env var references (`process.env.PASSWORD`, `import.meta.env.VITE_KEY`) —
|
|
23
|
+
* these are `member_expression`/`identifier` nodes, never `string` literals,
|
|
24
|
+
* so they are never candidates.
|
|
25
|
+
*/
|
|
26
|
+
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
27
|
+
import type { Violation } from '../../types.js';
|
|
28
|
+
import type { AST, LanguageAdapter } from '../../languages/types.js';
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for the Secrets analyzer.
|
|
31
|
+
*/
|
|
32
|
+
export interface SecretsAnalyzerConfig {
|
|
33
|
+
/** Toggle the hardcoded-secret check. Default true (default-on). */
|
|
34
|
+
checkHardcodedSecrets?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare const DEFAULT_SECRETS_CONFIG: SecretsAnalyzerConfig;
|
|
37
|
+
/**
|
|
38
|
+
* Flags hardcoded credentials, API keys, and tokens. Critical severity — a false
|
|
39
|
+
* positive blocks the edit loop, so detection is precision-first (see the file
|
|
40
|
+
* header for the near-miss classes that must stay silent).
|
|
41
|
+
*/
|
|
42
|
+
export declare class UniversalSecretsAnalyzer extends UniversalAnalyzer {
|
|
43
|
+
readonly name = "secrets";
|
|
44
|
+
readonly description = "Detects hardcoded credentials, API keys, and tokens";
|
|
45
|
+
readonly category = "security";
|
|
46
|
+
protected analyzeAST(ast: AST, adapter: LanguageAdapter, config: SecretsAnalyzerConfig, sourceCode: string): Promise<Violation[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Inspect one AST node for hardcoded credentials in a credential position;
|
|
49
|
+
* returns the violations to report (possibly empty).
|
|
50
|
+
*/
|
|
51
|
+
private inspectNode;
|
|
52
|
+
private checkCredentialCall;
|
|
53
|
+
private makeViolation;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=UniversalSecretsAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UniversalSecretsAnalyzer.d.ts","sourceRoot":"","sources":["../../../src/analyzers/universal/UniversalSecretsAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAGzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAW,MAAM,0BAA0B,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,eAAO,MAAM,sBAAsB,EAAE,qBAEpC,CAAC;AAoMF;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,iBAAiB;IAC7D,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,WAAW,yDAAyD;IAC7E,QAAQ,CAAC,QAAQ,cAAc;IAE/B,UAAgB,UAAU,CACxB,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,qBAAqB,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,SAAS,EAAE,CAAC,CAatB;IAED;;;OAGG;IACH,OAAO,CAAC,WAAW;IA4CnB,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,aAAa;CAsBtB"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Secrets Analyzer
|
|
3
|
+
* Detects hardcoded credentials, API keys, and tokens in source.
|
|
4
|
+
*
|
|
5
|
+
* The reference case is a Playwright/Puppeteer automation script typing a real
|
|
6
|
+
* password into a login form:
|
|
7
|
+
*
|
|
8
|
+
* page.type('#password', 'vyy8AUVvish34Fq')
|
|
9
|
+
*
|
|
10
|
+
* A secret is a string literal that (a) sits in a credential position — a
|
|
11
|
+
* variable/field/object-key named like a secret, or a call argument whose
|
|
12
|
+
* sibling argument is a credential selector (`#password`) — and (b) looks like
|
|
13
|
+
* a real value rather than a placeholder (`'password'`, `'your-api-key'`,
|
|
14
|
+
* `'changeme'`).
|
|
15
|
+
*
|
|
16
|
+
* Near-miss classes that must stay silent (critical severity gates — a false
|
|
17
|
+
* positive blocks the edit loop, so precision is the design constraint):
|
|
18
|
+
* - test fixtures / mocks (`.test.*`, `.spec.*`, `__tests__`, `fixtures`) —
|
|
19
|
+
* excluded by file path, before any literal is read;
|
|
20
|
+
* - placeholder values (`'changeme'`, `'your-api-key'`, `'<token>'`) —
|
|
21
|
+
* rejected by the value heuristic;
|
|
22
|
+
* - env var references (`process.env.PASSWORD`, `import.meta.env.VITE_KEY`) —
|
|
23
|
+
* these are `member_expression`/`identifier` nodes, never `string` literals,
|
|
24
|
+
* so they are never candidates.
|
|
25
|
+
*/
|
|
26
|
+
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
27
|
+
import { withRuleTiming } from '../ruleTiming.js';
|
|
28
|
+
import { walkAST } from '../../languages/adapterBridge.js';
|
|
29
|
+
export const DEFAULT_SECRETS_CONFIG = {
|
|
30
|
+
checkHardcodedSecrets: true,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Secret-ish names, normalized (lowercased, non-alphanumerics stripped) so
|
|
34
|
+
* `apiKey`, `api_key`, `API_KEY`, and `x-api-key` all collapse to one form.
|
|
35
|
+
* Matched against variable names, object keys, field names, and the core of
|
|
36
|
+
* credential selectors (`#password` → `password`).
|
|
37
|
+
*/
|
|
38
|
+
const SECRET_NAMES = new Set([
|
|
39
|
+
'password', 'passwd', 'pwd', 'passphrase',
|
|
40
|
+
'secret', 'clientsecret',
|
|
41
|
+
'apikey',
|
|
42
|
+
'accesskey', 'accesstoken',
|
|
43
|
+
'authtoken',
|
|
44
|
+
'token',
|
|
45
|
+
'privatekey',
|
|
46
|
+
'credential', 'credentials',
|
|
47
|
+
'awssecret', 'awskey',
|
|
48
|
+
'sessionkey', 'signingkey',
|
|
49
|
+
'authorization',
|
|
50
|
+
'xapikey', 'xauthtoken',
|
|
51
|
+
]);
|
|
52
|
+
function normalizeName(name) {
|
|
53
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
54
|
+
}
|
|
55
|
+
function isSecretName(name) {
|
|
56
|
+
const norm = normalizeName(name);
|
|
57
|
+
return norm.length > 0 && SECRET_NAMES.has(norm);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract the raw text of a `string` literal, preferring the `string_fragment`
|
|
61
|
+
* child (the content without quotes/delimiters) and falling back to stripping
|
|
62
|
+
* surrounding quotes from the node text.
|
|
63
|
+
*/
|
|
64
|
+
function stringValue(node, adapter, sourceCode) {
|
|
65
|
+
const frag = node.children?.find((c) => c.type === 'string_fragment');
|
|
66
|
+
if (frag)
|
|
67
|
+
return adapter.getNodeText(frag, sourceCode);
|
|
68
|
+
let text = adapter.getNodeText(node, sourceCode).trim();
|
|
69
|
+
if ((text.startsWith("'") && text.endsWith("'")) ||
|
|
70
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
71
|
+
(text.startsWith('`') && text.endsWith('`'))) {
|
|
72
|
+
text = text.slice(1, -1);
|
|
73
|
+
}
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
/** Find a direct `string` child (the literal value), not a nested one. */
|
|
77
|
+
function directStringChild(node, adapter, sourceCode) {
|
|
78
|
+
for (const c of node.children ?? []) {
|
|
79
|
+
if (c.type === 'string')
|
|
80
|
+
return stringValue(c, adapter, sourceCode);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/** A symbol that is not a common word separator (`-`, `_`, `.`, space). */
|
|
85
|
+
const HARD_SYMBOL = /[^A-Za-z0-9\-_.\s]/;
|
|
86
|
+
/**
|
|
87
|
+
* A known token prefix (`Bearer …`, `sk-…`, `ghp_…`, `AKIA…`, `xoxb-…`, JWT
|
|
88
|
+
* `eyJ…`) marks a value as a secret on its own, even when it is short.
|
|
89
|
+
*/
|
|
90
|
+
function hasKnownTokenPrefix(value) {
|
|
91
|
+
const t = value.trim();
|
|
92
|
+
return (/^Bearer\s/i.test(t) ||
|
|
93
|
+
/^sk-[A-Za-z0-9]{20,}/.test(t) || // OpenAI
|
|
94
|
+
/^(ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9]{20,}/.test(t) || // GitHub
|
|
95
|
+
/^(sk|rk|pk)_(live|test)_[A-Za-z0-9]{10,}/.test(t) || // Stripe
|
|
96
|
+
/^AKIA[0-9A-Z]{16}$/.test(t) || // AWS access key
|
|
97
|
+
/^xox[baprs]-/.test(t) || // Slack
|
|
98
|
+
/^eyJ[A-Za-z0-9_-]{10,}\./.test(t) // JWT
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const PLACEHOLDERS = new Set([
|
|
102
|
+
'password', 'passwd', 'pwd', 'changeme', 'changethis', 'secret', 'yoursecret',
|
|
103
|
+
'yourapikey', 'yourtoken', 'yourapitoken', 'apikey', 'token', 'example',
|
|
104
|
+
'dummy', 'placeholder', 'replaceme', 'todo', 'fixme', 'xxx', 'xxxx', 'xxxxx',
|
|
105
|
+
'test', 'testpassword', 'testsecret', 'testtoken', '123456', '12345678',
|
|
106
|
+
'123456789', 'password123', 'qwerty', 'letmein', 'abc123', 'foobar',
|
|
107
|
+
'helloworld', 'notareal', 'notreal', 'sample', 'samplekey', 'sampletoken',
|
|
108
|
+
'insecure', 'insecurepassword', 'notasecret',
|
|
109
|
+
]);
|
|
110
|
+
/** Reject placeholder/example values: templates, redactions, and common words. */
|
|
111
|
+
function isPlaceholder(value) {
|
|
112
|
+
const t = value.trim();
|
|
113
|
+
if (t.length === 0)
|
|
114
|
+
return true;
|
|
115
|
+
if (/^<[^>]+>$/.test(t))
|
|
116
|
+
return true; // <your-token>
|
|
117
|
+
if (/^[.…]{3,}$/.test(t))
|
|
118
|
+
return true; // ...
|
|
119
|
+
if (/^[*xX]{3,}$/.test(t))
|
|
120
|
+
return true; // *** or xxx
|
|
121
|
+
if (/^redacted$/i.test(t))
|
|
122
|
+
return true;
|
|
123
|
+
const norm = normalizeName(t);
|
|
124
|
+
if (PLACEHOLDERS.has(norm))
|
|
125
|
+
return true;
|
|
126
|
+
if (/^(your|test|example|sample|dummy|placeholder|replaceme|changeme)/.test(norm))
|
|
127
|
+
return true;
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Does this literal look like a real credential value rather than a placeholder?
|
|
132
|
+
*
|
|
133
|
+
* Precision-first: the value must either carry a known token prefix or be long
|
|
134
|
+
* enough (≥8 chars) and show character variety (a digit, an uppercase letter,
|
|
135
|
+
* or a non-separator symbol). Pure-lowercase words (`changeme`) and hyphenated
|
|
136
|
+
* phrases (`your-api-key`) fail this; high-entropy values (`vyy8AUVvish34Fq`)
|
|
137
|
+
* pass. Emails and URLs are explicitly not secrets.
|
|
138
|
+
*/
|
|
139
|
+
function looksLikeRealSecret(value) {
|
|
140
|
+
const t = value.trim();
|
|
141
|
+
if (isPlaceholder(t))
|
|
142
|
+
return false;
|
|
143
|
+
if (hasKnownTokenPrefix(t))
|
|
144
|
+
return true;
|
|
145
|
+
if (t.length < 8)
|
|
146
|
+
return false;
|
|
147
|
+
if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(t))
|
|
148
|
+
return false; // email
|
|
149
|
+
if (/^https?:\/\//i.test(t))
|
|
150
|
+
return false; // URL
|
|
151
|
+
const hasDigit = /\d/.test(t);
|
|
152
|
+
const hasUpper = /[A-Z]/.test(t);
|
|
153
|
+
const hasHardSymbol = HARD_SYMBOL.test(t);
|
|
154
|
+
return hasDigit || hasUpper || hasHardSymbol;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Is this path a test/fixture file? Excluded up front — a hardcoded credential
|
|
158
|
+
* in a test fixture or mock is expected and must not block the edit loop.
|
|
159
|
+
*/
|
|
160
|
+
function isTestOrFixtureFile(filePath) {
|
|
161
|
+
const lower = filePath.toLowerCase();
|
|
162
|
+
return (lower.includes('.test.') || lower.includes('.spec.') ||
|
|
163
|
+
lower.includes('__tests__') ||
|
|
164
|
+
lower.includes('/test/') || lower.includes('/tests/') ||
|
|
165
|
+
lower.includes('/fixtures/') || lower.includes('.fixture.') ||
|
|
166
|
+
lower.endsWith('_test.go'));
|
|
167
|
+
}
|
|
168
|
+
/** Extract the variable name from a `variable_declarator`. */
|
|
169
|
+
function declaratorName(node, adapter, sourceCode) {
|
|
170
|
+
const name = node.children?.find((c) => c.type === 'identifier');
|
|
171
|
+
return name ? adapter.getNodeText(name, sourceCode) : null;
|
|
172
|
+
}
|
|
173
|
+
/** Extract the assigned field name from an `assignment_expression` left side. */
|
|
174
|
+
function assignmentKey(node, adapter, sourceCode) {
|
|
175
|
+
const left = node.children?.find((c) => c.type === 'member_expression' || c.type === 'subscript_expression' || c.type === 'identifier');
|
|
176
|
+
if (!left)
|
|
177
|
+
return null;
|
|
178
|
+
if (left.type === 'member_expression') {
|
|
179
|
+
const prop = left.children?.find((c) => c.type === 'property_identifier');
|
|
180
|
+
return prop ? adapter.getNodeText(prop, sourceCode) : null;
|
|
181
|
+
}
|
|
182
|
+
if (left.type === 'subscript_expression') {
|
|
183
|
+
const key = left.children?.find((c) => c.type === 'string');
|
|
184
|
+
return key ? stringValue(key, adapter, sourceCode) : null;
|
|
185
|
+
}
|
|
186
|
+
return adapter.getNodeText(left, sourceCode);
|
|
187
|
+
}
|
|
188
|
+
/** Extract `{ key, value }` from an object `pair` node. */
|
|
189
|
+
function pairKeyAndValue(node, adapter, sourceCode) {
|
|
190
|
+
const strings = (node.children ?? []).filter((c) => c.type === 'string');
|
|
191
|
+
if (strings.length === 0)
|
|
192
|
+
return null;
|
|
193
|
+
const keyNode = node.children?.find((c) => c.type === 'property_identifier') ?? strings[0];
|
|
194
|
+
const valueNode = strings[strings.length - 1];
|
|
195
|
+
if (valueNode === keyNode && strings.length < 2)
|
|
196
|
+
return null;
|
|
197
|
+
const key = keyNode.type === 'property_identifier'
|
|
198
|
+
? adapter.getNodeText(keyNode, sourceCode)
|
|
199
|
+
: stringValue(keyNode, adapter, sourceCode);
|
|
200
|
+
const value = stringValue(valueNode, adapter, sourceCode);
|
|
201
|
+
return { key, value };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Is this string a credential *field selector* (`#password`, `.api_key`,
|
|
205
|
+
* `input[name=token]`)? Extracts the field-name core and checks it against the
|
|
206
|
+
* secret-name set.
|
|
207
|
+
*/
|
|
208
|
+
function isCredentialSelector(text) {
|
|
209
|
+
let core = text.trim().replace(/^['"]|['"]$/g, '');
|
|
210
|
+
core = core.replace(/^[.#]/, '').trim();
|
|
211
|
+
const bracket = core.match(/\[([^\[\]]+)\]$/);
|
|
212
|
+
if (bracket)
|
|
213
|
+
core = bracket[1];
|
|
214
|
+
core = core.replace(/^name\s*=\s*["']?|["']?$/gi, '');
|
|
215
|
+
core = core.replace(/["']/g, '').trim();
|
|
216
|
+
return isSecretName(core);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Flags hardcoded credentials, API keys, and tokens. Critical severity — a false
|
|
220
|
+
* positive blocks the edit loop, so detection is precision-first (see the file
|
|
221
|
+
* header for the near-miss classes that must stay silent).
|
|
222
|
+
*/
|
|
223
|
+
export class UniversalSecretsAnalyzer extends UniversalAnalyzer {
|
|
224
|
+
name = 'secrets';
|
|
225
|
+
description = 'Detects hardcoded credentials, API keys, and tokens';
|
|
226
|
+
category = 'security';
|
|
227
|
+
async analyzeAST(ast, adapter, config, sourceCode) {
|
|
228
|
+
const violations = [];
|
|
229
|
+
const finalConfig = { ...DEFAULT_SECRETS_CONFIG, ...config };
|
|
230
|
+
if (finalConfig.checkHardcodedSecrets === false)
|
|
231
|
+
return violations;
|
|
232
|
+
if (isTestOrFixtureFile(ast.filePath))
|
|
233
|
+
return violations;
|
|
234
|
+
withRuleTiming('hardcoded-secret', () => {
|
|
235
|
+
walkAST(ast.root, (node) => {
|
|
236
|
+
violations.push(...this.inspectNode(node, adapter, sourceCode, ast.filePath));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return violations;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Inspect one AST node for hardcoded credentials in a credential position;
|
|
243
|
+
* returns the violations to report (possibly empty).
|
|
244
|
+
*/
|
|
245
|
+
inspectNode(node, adapter, sourceCode, filePath) {
|
|
246
|
+
// 1. `const <name> = '<value>'`
|
|
247
|
+
if (node.type === 'variable_declarator') {
|
|
248
|
+
const name = declaratorName(node, adapter, sourceCode);
|
|
249
|
+
const value = directStringChild(node, adapter, sourceCode);
|
|
250
|
+
if (name && value !== null && isSecretName(name) && looksLikeRealSecret(value)) {
|
|
251
|
+
return [this.makeViolation(filePath, node, name, value)];
|
|
252
|
+
}
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
// 2. `<obj>.<field> = '<value>'` / `<field> = '<value>'`
|
|
256
|
+
if (node.type === 'assignment_expression') {
|
|
257
|
+
const key = assignmentKey(node, adapter, sourceCode);
|
|
258
|
+
const value = directStringChild(node, adapter, sourceCode);
|
|
259
|
+
if (key && value !== null && isSecretName(key) && looksLikeRealSecret(value)) {
|
|
260
|
+
return [this.makeViolation(filePath, node, key, value)];
|
|
261
|
+
}
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
// 3. `{ <key>: '<value>' }`
|
|
265
|
+
if (node.type === 'pair') {
|
|
266
|
+
const pv = pairKeyAndValue(node, adapter, sourceCode);
|
|
267
|
+
if (pv && isSecretName(pv.key) && looksLikeRealSecret(pv.value)) {
|
|
268
|
+
return [this.makeViolation(filePath, node, pv.key, pv.value)];
|
|
269
|
+
}
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
// 4. `fn('<selector>', '<secret>')` — a credential selector string plus a
|
|
273
|
+
// sibling secret-value string (the page.type reference case).
|
|
274
|
+
if (node.type === 'call_expression') {
|
|
275
|
+
return this.checkCredentialCall(node, adapter, sourceCode, filePath);
|
|
276
|
+
}
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
checkCredentialCall(node, adapter, sourceCode, filePath) {
|
|
280
|
+
const argsNode = node.children?.find((c) => c.type === 'arguments');
|
|
281
|
+
if (!argsNode)
|
|
282
|
+
return [];
|
|
283
|
+
const stringArgs = (argsNode.children ?? []).filter((c) => c.type === 'string');
|
|
284
|
+
if (stringArgs.length < 2)
|
|
285
|
+
return [];
|
|
286
|
+
const hasSelector = stringArgs.some((a) => isCredentialSelector(stringValue(a, adapter, sourceCode)));
|
|
287
|
+
if (!hasSelector)
|
|
288
|
+
return [];
|
|
289
|
+
const found = [];
|
|
290
|
+
for (const a of stringArgs) {
|
|
291
|
+
const value = stringValue(a, adapter, sourceCode);
|
|
292
|
+
if (isCredentialSelector(value))
|
|
293
|
+
continue; // the selector is not the secret
|
|
294
|
+
if (looksLikeRealSecret(value)) {
|
|
295
|
+
found.push(this.makeViolation(filePath, node, undefined, value));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return found;
|
|
299
|
+
}
|
|
300
|
+
makeViolation(file, node, name, value) {
|
|
301
|
+
const label = name ? ` "${name}"` : '';
|
|
302
|
+
const reference = name
|
|
303
|
+
? `process.env.${normalizeName(name).toUpperCase()}`
|
|
304
|
+
: 'process.env.SECRET';
|
|
305
|
+
return this.createViolation(file, node.location.start, `Hardcoded secret${label} detected: a ${value.length}-character credential is embedded in source. Move it to an environment variable or secret store.`, {
|
|
306
|
+
severity: 'critical',
|
|
307
|
+
rule: 'hardcoded-secret',
|
|
308
|
+
resolution: {
|
|
309
|
+
action: 'remove-hardcoded-secret',
|
|
310
|
+
summary: `Replace the hardcoded${label} credential with a reference to an environment variable or secret store (e.g. ${reference}).`,
|
|
311
|
+
symbols: name ? [name] : undefined,
|
|
312
|
+
files: [file],
|
|
313
|
+
lines: [node.location.start.line],
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=UniversalSecretsAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UniversalSecretsAnalyzer.js","sourceRoot":"","sources":["../../../src/analyzers/universal/UniversalSecretsAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAY3D,MAAM,CAAC,MAAM,sBAAsB,GAA0B;IAC3D,qBAAqB,EAAE,IAAI;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY;IACzC,QAAQ,EAAE,cAAc;IACxB,QAAQ;IACR,WAAW,EAAE,aAAa;IAC1B,WAAW;IACX,OAAO;IACP,YAAY;IACZ,YAAY,EAAE,aAAa;IAC3B,WAAW,EAAE,QAAQ;IACrB,YAAY,EAAE,YAAY;IAC1B,eAAe;IACf,SAAS,EAAE,YAAY;CACxB,CAAC,CAAC;AAEH,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,IAAa,EAAE,OAAwB,EAAE,UAAkB;IAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC;IACtE,IAAI,IAAI;QAAE,OAAO,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACvD,IAAI,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IACE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAC5C,CAAC;QACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0EAA0E;AAC1E,SAAS,iBAAiB,CAAC,IAAa,EAAE,OAAwB,EAAE,UAAkB;IACpF,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QACpC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,2EAA2E;AAC3E,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAEzC;;;GAGG;AACH,SAAS,mBAAmB,CAAC,KAAa;IACxC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACvB,OAAO,CACL,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACpB,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,IAA4B,SAAS;QACnE,gDAAgD,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,SAAS;QACrE,0CAA0C,CAAC,IAAI,CAAC,CAAC,CAAC,IAAQ,SAAS;QACnE,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,IAA8B,iBAAiB;QAC3E,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,IAAoC,QAAQ;QAClE,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAwB,MAAM;KACjE,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY;IAC7E,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS;IACvE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO;IAC5E,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU;IACvE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ;IACnE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa;IACzE,UAAU,EAAE,kBAAkB,EAAE,YAAY;CAC7C,CAAC,CAAC;AAEH,kFAAkF;AAClF,SAAS,aAAa,CAAC,KAAa;IAClC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACvB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,CAAQ,eAAe;IAC5D,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,CAAO,MAAM;IACnD,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,CAAM,aAAa;IAC1D,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,kEAAkE,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/F,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,mBAAmB,CAAC,KAAa;IACxC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACvB,IAAI,aAAa,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,mBAAmB,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,IAAI,4BAA4B,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,QAAQ;IAChE,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,CAAe,MAAM;IAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjC,MAAM,aAAa,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,QAAQ,IAAI,QAAQ,IAAI,aAAa,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,QAAgB;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,OAAO,CACL,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpD,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC3B,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;QACrD,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC3D,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,8DAA8D;AAC9D,SAAS,cAAc,CAAC,IAAa,EAAE,OAAwB,EAAE,UAAkB;IACjF,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED,iFAAiF;AACjF,SAAS,aAAa,CAAC,IAAa,EAAE,OAAwB,EAAE,UAAkB;IAChF,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAC9B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,mBAAmB,IAAI,CAAC,CAAC,IAAI,KAAK,sBAAsB,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,CACtG,CAAC;IACF,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7D,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QAC5D,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5D,CAAC;IACD,OAAO,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAC/C,CAAC;AAED,2DAA2D;AAC3D,SAAS,eAAe,CACtB,IAAa,EACb,OAAwB,EACxB,UAAkB;IAElB,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACzE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9C,IAAI,SAAS,KAAK,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,KAAK,qBAAqB;QAChD,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC;QAC1C,CAAC,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAC1D,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,IAAY;IACxC,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACnD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC9C,IAAI,OAAO;QAAE,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;IACtD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,wBAAyB,SAAQ,iBAAiB;IACpD,IAAI,GAAG,SAAS,CAAC;IACjB,WAAW,GAAG,qDAAqD,CAAC;IACpE,QAAQ,GAAG,UAAU,CAAC;IAErB,KAAK,CAAC,UAAU,CACxB,GAAQ,EACR,OAAwB,EACxB,MAA6B,EAC7B,UAAkB;QAElB,MAAM,UAAU,GAAgB,EAAE,CAAC;QACnC,MAAM,WAAW,GAAG,EAAE,GAAG,sBAAsB,EAAE,GAAG,MAAM,EAAE,CAAC;QAC7D,IAAI,WAAW,CAAC,qBAAqB,KAAK,KAAK;YAAE,OAAO,UAAU,CAAC;QACnE,IAAI,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,UAAU,CAAC;QAEzD,cAAc,CAAC,kBAAkB,EAAE,GAAG,EAAE;YACtC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;gBACzB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;YAChF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,OAAO,UAAU,CAAC;IACpB,CAAC;IAED;;;OAGG;IACK,WAAW,CACjB,IAAa,EACb,OAAwB,EACxB,UAAkB,EAClB,QAAgB;QAEhB,gCAAgC;QAChC,IAAI,IAAI,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YACvD,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YAC3D,IAAI,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/E,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YAC3D,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,yDAAyD;QACzD,IAAI,IAAI,CAAC,IAAI,KAAK,uBAAuB,EAAE,CAAC;YAC1C,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YAC3D,IAAI,GAAG,IAAI,KAAK,KAAK,IAAI,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC7E,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;YAC1D,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,4BAA4B;QAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,MAAM,EAAE,GAAG,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YACtD,IAAI,EAAE,IAAI,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChE,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;YAChE,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,0EAA0E;QAC1E,iEAAiE;QACjE,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;QACvE,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;IAEO,mBAAmB,CACzB,IAAa,EACb,OAAwB,EACxB,UAAkB,EAClB,QAAgB;QAEhB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;QACpE,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,CAAC,QAAQ,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QAChF,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAErC,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;QACtG,IAAI,CAAC,WAAW;YAAE,OAAO,EAAE,CAAC;QAE5B,MAAM,KAAK,GAAgB,EAAE,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YAClD,IAAI,oBAAoB,CAAC,KAAK,CAAC;gBAAE,SAAS,CAAC,iCAAiC;YAC5E,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,aAAa,CAAC,IAAY,EAAE,IAAa,EAAE,IAAwB,EAAE,KAAa;QACxF,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,IAAI;YACpB,CAAC,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE;YACpD,CAAC,CAAC,oBAAoB,CAAC;QACzB,OAAO,IAAI,CAAC,eAAe,CACzB,IAAI,EACJ,IAAI,CAAC,QAAQ,CAAC,KAAK,EACnB,mBAAmB,KAAK,gBAAgB,KAAK,CAAC,MAAM,kGAAkG,EACtJ;YACE,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,kBAAkB;YACxB,UAAU,EAAE;gBACV,MAAM,EAAE,yBAAyB;gBACjC,OAAO,EAAE,wBAAwB,KAAK,iFAAiF,SAAS,IAAI;gBACpI,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;gBAClC,KAAK,EAAE,CAAC,IAAI,CAAC;gBACb,KAAK,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;aAClC;SACF,CACF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -90,16 +90,10 @@ declare abstract class UniversalStylesAnalyzerDetectors extends UniversalStylesA
|
|
|
90
90
|
*/
|
|
91
91
|
protected detectExactValueDrift(property: string, decls: StyleDeclRow[], cfg: StylesAnalyzerConfig): Violation[];
|
|
92
92
|
/**
|
|
93
|
-
* For scale-family properties (margin, padding, gap, font-size),
|
|
94
|
-
*
|
|
95
|
-
* and flag values that don't fit the scale.
|
|
93
|
+
* For scale-family properties (margin, padding, gap, font-size), flag values
|
|
94
|
+
* that are not members of the property's Tailwind design scale.
|
|
96
95
|
*/
|
|
97
96
|
protected detectOffScaleValues(byProperty: Map<string, StyleDeclRow[]>, cfg: StylesAnalyzerConfig): Violation[];
|
|
98
|
-
/**
|
|
99
|
-
* Infer the dominant scale step from a set of px values.
|
|
100
|
-
* Uses the Tailwind scale as candidate steps.
|
|
101
|
-
*/
|
|
102
|
-
protected inferScaleStep(values: number[]): number | null;
|
|
103
97
|
}
|
|
104
98
|
/**
|
|
105
99
|
* Universal styles analyzer.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UniversalStylesAnalyzer.d.ts","sourceRoot":"","sources":["../../../src/analyzers/universal/UniversalStylesAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,KAAK,EAAE,cAAc,EAAe,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACzF,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAUrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAQ3D,eAAO,MAAM,qBAAqB,EAAE,oBAcnC,CAAC;
|
|
1
|
+
{"version":3,"file":"UniversalStylesAnalyzer.d.ts","sourceRoot":"","sources":["../../../src/analyzers/universal/UniversalStylesAnalyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -34,11 +34,19 @@ export const DEFAULT_STYLES_CONFIG = {
|
|
|
34
34
|
declarationSetSimilarityThreshold: 0.9,
|
|
35
35
|
};
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
// Tailwind
|
|
37
|
+
// Tailwind default design scales (px equivalents)
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
|
+
//
|
|
40
|
+
// Two distinct scales: the *spacing* scale (margin/padding/gap) and the
|
|
41
|
+
// *font-size* scale. They are not interchangeable — `14px` is a valid font size
|
|
42
|
+
// (`text-sm`) but not a named spacing step, and `28px` is a valid spacing step
|
|
43
|
+
// (`spacing-7`) but not a named font size. The old proxy collapsed both onto a
|
|
44
|
+
// single inferred step, which is why `font-size` was silently judged against a
|
|
45
|
+
// spacing step it never uses.
|
|
39
46
|
const TAILWIND_SPACING_PX = {
|
|
40
47
|
'0': 0, 'px': 1, '0.5': 2,
|
|
41
|
-
'1': 4, '
|
|
48
|
+
'1': 4, '1.5': 6, '2': 8, '2.5': 10,
|
|
49
|
+
'3': 12, '3.5': 14, '4': 16,
|
|
42
50
|
'5': 20, '6': 24, '7': 28, '8': 32,
|
|
43
51
|
'9': 36, '10': 40, '11': 44, '12': 48,
|
|
44
52
|
'14': 56, '16': 64, '20': 80, '24': 96,
|
|
@@ -47,7 +55,35 @@ const TAILWIND_SPACING_PX = {
|
|
|
47
55
|
'60': 240, '64': 256, '72': 288, '80': 320,
|
|
48
56
|
'96': 384,
|
|
49
57
|
};
|
|
50
|
-
|
|
58
|
+
/** Named Tailwind font-size steps (`text-xs` … `text-9xl`), px equivalents. */
|
|
59
|
+
const TAILWIND_FONT_SIZE_PX = [
|
|
60
|
+
12, 14, 16, 18, 20, 24, 30, 36, 48, 60, 72, 96, 128,
|
|
61
|
+
];
|
|
62
|
+
const TAILWIND_SPACING_VALUES = Object.values(TAILWIND_SPACING_PX).sort((a, b) => a - b);
|
|
63
|
+
/** O(1) membership sets for "is this px value on the Tailwind scale". */
|
|
64
|
+
const TAILWIND_SPACING_SET = new Set(TAILWIND_SPACING_VALUES);
|
|
65
|
+
const TAILWIND_FONT_SIZE_SET = new Set(TAILWIND_FONT_SIZE_PX);
|
|
66
|
+
/** The design scale a property belongs to. */
|
|
67
|
+
function scaleForProperty(property) {
|
|
68
|
+
if (property === 'font-size') {
|
|
69
|
+
return { values: TAILWIND_FONT_SIZE_PX, set: TAILWIND_FONT_SIZE_SET, label: 'font-size scale' };
|
|
70
|
+
}
|
|
71
|
+
return { values: TAILWIND_SPACING_VALUES, set: TAILWIND_SPACING_SET, label: 'spacing scale' };
|
|
72
|
+
}
|
|
73
|
+
/** The nearest scale values on either side of `px` (for the suggestion). */
|
|
74
|
+
function nearestScaleValues(px, values) {
|
|
75
|
+
let lower = 0;
|
|
76
|
+
let upper = values[values.length - 1];
|
|
77
|
+
for (const s of values) {
|
|
78
|
+
if (s <= px)
|
|
79
|
+
lower = s;
|
|
80
|
+
if (s >= px) {
|
|
81
|
+
upper = s;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [lower, upper];
|
|
86
|
+
}
|
|
51
87
|
/**
|
|
52
88
|
* Stable symbol for a value-scoped finding: `<property>: <value>`.
|
|
53
89
|
*
|
|
@@ -353,9 +389,8 @@ class UniversalStylesAnalyzerDetectors extends UniversalStylesAnalyzerBase {
|
|
|
353
389
|
// Detector 2: Off-Scale Values
|
|
354
390
|
// -----------------------------------------------------------------------
|
|
355
391
|
/**
|
|
356
|
-
* For scale-family properties (margin, padding, gap, font-size),
|
|
357
|
-
*
|
|
358
|
-
* and flag values that don't fit the scale.
|
|
392
|
+
* For scale-family properties (margin, padding, gap, font-size), flag values
|
|
393
|
+
* that are not members of the property's Tailwind design scale.
|
|
359
394
|
*/
|
|
360
395
|
detectOffScaleValues(byProperty, cfg) {
|
|
361
396
|
const violations = [];
|
|
@@ -373,51 +408,21 @@ class UniversalStylesAnalyzerDetectors extends UniversalStylesAnalyzerBase {
|
|
|
373
408
|
}
|
|
374
409
|
if (parsed.length < cfg.minCorpus)
|
|
375
410
|
continue;
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// Flag off-scale values
|
|
411
|
+
// Flag values that are not members of the property's scale. Spacing
|
|
412
|
+
// properties are judged against the spacing scale, `font-size` against
|
|
413
|
+
// the font-size scale — they are not interchangeable.
|
|
414
|
+
const scale = scaleForProperty(property);
|
|
381
415
|
for (const { decl, px } of parsed) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (Math.abs(remainder) > 1 && Math.abs(remainder - step) > 1) {
|
|
416
|
+
if (!scale.set.has(px)) {
|
|
417
|
+
const [lower, upper] = nearestScaleValues(px, scale.values);
|
|
385
418
|
violations.push(this.makeViolation(decl.file_path, decl.line, `Off-scale "${property}" value: "${decl.raw_value}" (${px}px) ` +
|
|
386
|
-
`
|
|
387
|
-
`
|
|
388
|
-
`${Math.ceil(px / step) * step}px.`, { severity: 'warning', rule: 'styles/off-scale', symbol: declValueKey(decl) }));
|
|
419
|
+
`is not on the Tailwind ${scale.label}. ` +
|
|
420
|
+
`Nearest scale values: ${lower}px or ${upper}px.`, { severity: 'warning', rule: 'styles/off-scale', symbol: declValueKey(decl) }));
|
|
389
421
|
}
|
|
390
422
|
}
|
|
391
423
|
}
|
|
392
424
|
return violations;
|
|
393
425
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Infer the dominant scale step from a set of px values.
|
|
396
|
-
* Uses the Tailwind scale as candidate steps.
|
|
397
|
-
*/
|
|
398
|
-
inferScaleStep(values) {
|
|
399
|
-
if (values.length < 3)
|
|
400
|
-
return null;
|
|
401
|
-
// Count how many values align with each tailwind step
|
|
402
|
-
const candidates = [2, 4, 8, 16];
|
|
403
|
-
let bestStep = 4;
|
|
404
|
-
let bestScore = 0;
|
|
405
|
-
for (const step of candidates) {
|
|
406
|
-
let score = 0;
|
|
407
|
-
for (const v of values) {
|
|
408
|
-
if (v % step === 0)
|
|
409
|
-
score++;
|
|
410
|
-
}
|
|
411
|
-
if (score > bestScore) {
|
|
412
|
-
bestScore = score;
|
|
413
|
-
bestStep = step;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
// Require at least 60% alignment
|
|
417
|
-
if (bestScore / values.length < 0.6)
|
|
418
|
-
return null;
|
|
419
|
-
return bestStep;
|
|
420
|
-
}
|
|
421
426
|
}
|
|
422
427
|
// ---------------------------------------------------------------------------
|
|
423
428
|
// Structure detectors: class/token/mechanism integrity checks over the raw
|
|
@@ -890,7 +895,11 @@ function buildTokenValueMap(tokens) {
|
|
|
890
895
|
if (t.file_path === 'built-in defaults')
|
|
891
896
|
continue;
|
|
892
897
|
const normalizedTokenVal = normalizeValue(t.value, '__token__');
|
|
893
|
-
|
|
898
|
+
// Key by the same normalized spelling the lookup uses, so a shorthand or
|
|
899
|
+
// case-differing token value (`#fff`, `#FFFFFF`) still matches a raw
|
|
900
|
+
// declaration of the same colour (`#ffffff`). The ledger's key-normalization
|
|
901
|
+
// asymmetry — raw key vs normalized lookup — was the last crude gap.
|
|
902
|
+
tokenValueMap.set(normalizeForTokenMatch(t.value), { name: t.name, valueType: normalizedTokenVal?.type ?? null });
|
|
894
903
|
}
|
|
895
904
|
return tokenValueMap;
|
|
896
905
|
}
|