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.
Files changed (153) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +73 -0
  3. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  4. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +12 -1
  5. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  6. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  7. package/dist/analyzers/cross-language/SchemaValidator.js +122 -43
  8. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  9. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
  10. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +6 -7
  11. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  12. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  13. package/dist/analyzers/ruleRegistry.js +152 -20
  14. package/dist/analyzers/ruleRegistry.js.map +1 -1
  15. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +38 -1
  16. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  17. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +437 -48
  18. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  19. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +6 -0
  20. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  21. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +117 -35
  22. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  23. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +86 -15
  25. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  26. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +32 -7
  27. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +104 -23
  29. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  30. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts +55 -0
  31. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -0
  32. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +318 -0
  33. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -0
  34. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -8
  35. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  36. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +54 -45
  37. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  38. package/dist/analyzers/universal/functionConcerns.d.ts +57 -0
  39. package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -0
  40. package/dist/analyzers/universal/functionConcerns.js +239 -0
  41. package/dist/analyzers/universal/functionConcerns.js.map +1 -0
  42. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  43. package/dist/analyzers/universal/schema/codeAnalysis.js +13 -7
  44. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  45. package/dist/analyzers/universal/schema/jsonSchema.js +124 -11
  46. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  47. package/dist/auditRouter.d.ts.map +1 -1
  48. package/dist/auditRouter.js +4 -6
  49. package/dist/auditRouter.js.map +1 -1
  50. package/dist/auditRunner.d.ts.map +1 -1
  51. package/dist/auditRunner.js +74 -48
  52. package/dist/auditRunner.js.map +1 -1
  53. package/dist/cli.js +321 -98
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config/configLoader.d.ts.map +1 -1
  56. package/dist/config/configLoader.js +23 -0
  57. package/dist/config/configLoader.js.map +1 -1
  58. package/dist/config/defaults.d.ts +6 -0
  59. package/dist/config/defaults.d.ts.map +1 -1
  60. package/dist/config/defaults.js +45 -10
  61. package/dist/config/defaults.js.map +1 -1
  62. package/dist/config/effectiveConfig.d.ts.map +1 -1
  63. package/dist/config/effectiveConfig.js +2 -0
  64. package/dist/config/effectiveConfig.js.map +1 -1
  65. package/dist/conventions/conventionMiner.d.ts +18 -3
  66. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  67. package/dist/conventions/conventionMiner.js +130 -12
  68. package/dist/conventions/conventionMiner.js.map +1 -1
  69. package/dist/daemon/core.d.ts +128 -0
  70. package/dist/daemon/core.d.ts.map +1 -0
  71. package/dist/daemon/core.js +700 -0
  72. package/dist/daemon/core.js.map +1 -0
  73. package/dist/daemon/lspServer.d.ts +51 -0
  74. package/dist/daemon/lspServer.d.ts.map +1 -0
  75. package/dist/daemon/lspServer.js +168 -0
  76. package/dist/daemon/lspServer.js.map +1 -0
  77. package/dist/daemon/main.d.ts +34 -0
  78. package/dist/daemon/main.d.ts.map +1 -0
  79. package/dist/daemon/main.js +147 -0
  80. package/dist/daemon/main.js.map +1 -0
  81. package/dist/daemon/resolve.d.ts +41 -0
  82. package/dist/daemon/resolve.d.ts.map +1 -0
  83. package/dist/daemon/resolve.js +90 -0
  84. package/dist/daemon/resolve.js.map +1 -0
  85. package/dist/daemon/socketClient.d.ts +21 -0
  86. package/dist/daemon/socketClient.d.ts.map +1 -0
  87. package/dist/daemon/socketClient.js +60 -0
  88. package/dist/daemon/socketClient.js.map +1 -0
  89. package/dist/daemon/socketServer.d.ts +33 -0
  90. package/dist/daemon/socketServer.d.ts.map +1 -0
  91. package/dist/daemon/socketServer.js +106 -0
  92. package/dist/daemon/socketServer.js.map +1 -0
  93. package/dist/daemon/types.d.ts +78 -0
  94. package/dist/daemon/types.d.ts.map +1 -0
  95. package/dist/daemon/types.js +10 -0
  96. package/dist/daemon/types.js.map +1 -0
  97. package/dist/dataPaths.d.ts +21 -0
  98. package/dist/dataPaths.d.ts.map +1 -1
  99. package/dist/dataPaths.js +27 -0
  100. package/dist/dataPaths.js.map +1 -1
  101. package/dist/graph/importGraph.d.ts +11 -0
  102. package/dist/graph/importGraph.d.ts.map +1 -1
  103. package/dist/graph/importGraph.js +11 -3
  104. package/dist/graph/importGraph.js.map +1 -1
  105. package/dist/hooks/core.d.ts.map +1 -1
  106. package/dist/hooks/core.js +21 -4
  107. package/dist/hooks/core.js.map +1 -1
  108. package/dist/installer.d.ts.map +1 -1
  109. package/dist/installer.js +2 -5
  110. package/dist/installer.js.map +1 -1
  111. package/dist/languages/RuntimeManager.js +10 -0
  112. package/dist/languages/RuntimeManager.js.map +1 -1
  113. package/dist/languages/go/analyzer +0 -0
  114. package/dist/languages/go/analyzer-src/analyzer.go +166 -35
  115. package/dist/languages/go/analyzer-src/solid.go +80 -172
  116. package/dist/languages/go/analyzer-src/types.go +18 -18
  117. package/dist/ledger.d.ts +1 -1
  118. package/dist/ledger.d.ts.map +1 -1
  119. package/dist/ledger.js +3 -3
  120. package/dist/ledger.js.map +1 -1
  121. package/dist/mcp.js +1 -1
  122. package/dist/mcp.js.map +1 -1
  123. package/dist/mcpAuditJobs.js +1 -1
  124. package/dist/mcpAuditJobs.js.map +1 -1
  125. package/dist/nextFileIncremental.d.ts +9 -3
  126. package/dist/nextFileIncremental.d.ts.map +1 -1
  127. package/dist/nextFileIncremental.js +32 -6
  128. package/dist/nextFileIncremental.js.map +1 -1
  129. package/dist/pipeline.d.ts +26 -0
  130. package/dist/pipeline.d.ts.map +1 -1
  131. package/dist/pipeline.js +86 -10
  132. package/dist/pipeline.js.map +1 -1
  133. package/dist/pipelineAdapters.d.ts +2 -1
  134. package/dist/pipelineAdapters.d.ts.map +1 -1
  135. package/dist/pipelineAdapters.js +228 -5
  136. package/dist/pipelineAdapters.js.map +1 -1
  137. package/dist/reporting/csvReportGenerator.js +1 -1
  138. package/dist/ruleAliases.d.ts.map +1 -1
  139. package/dist/ruleAliases.js +41 -2
  140. package/dist/ruleAliases.js.map +1 -1
  141. package/dist/styles/tailwindProbe.d.ts +8 -0
  142. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  143. package/dist/styles/tailwindProbe.js +11 -1
  144. package/dist/styles/tailwindProbe.js.map +1 -1
  145. package/dist/types/crossLanguage.d.ts +1 -0
  146. package/dist/types/crossLanguage.d.ts.map +1 -1
  147. package/dist/types.d.ts +30 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/types.js.map +1 -1
  150. package/package.json +7 -3
  151. package/plugin/.claude-plugin/plugin.json +1 -1
  152. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +4 -2
  153. 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":"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"}
@@ -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
- * infer the project scale from modal values + Tailwind defaults,
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":"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"}
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 v4 default spacing scale (px equivalents)
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, '2': 8, '3': 12, '4': 16,
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
- const TAILWIND_SCALE_VALUES = Object.values(TAILWIND_SPACING_PX).sort((a, b) => a - b);
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
- * infer the project scale from modal values + Tailwind defaults,
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
- // Infer the project scale step
377
- const step = this.inferScaleStep(parsed.map(p => p.px));
378
- if (step === null || step === 0)
379
- continue;
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
- const remainder = px % step;
383
- // Allow near-zero remainders (floating point tolerance: < 1px)
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
- `does not align with the inferred ${step}px scale step. ` +
387
- `Nearby scale values: ${Math.floor(px / step) * step}px or ` +
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
- tokenValueMap.set(t.value, { name: t.name, valueType: normalizedTokenVal?.type ?? null });
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
  }