@chemx/create-chemx 26.9.17-309 → 26.9.17-606
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 +63 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +8 -11
- package/cli/audit/ast-visitors.js +73 -0
- package/cli/audit/extended-visitors.js +239 -0
- package/cli/audit/reporter-utils.js +13 -22
- package/cli/audit/rules-registry.js +25 -0
- package/cli/audit/rules.spec.js +205 -0
- package/cli/build.d.ts +2 -1
- package/cli/build.js +8 -5
- package/cli/columnar.d.ts +14 -0
- package/cli/columnar.js +52 -0
- package/cli/columnar.spec.js +49 -0
- package/cli/help.js +29 -4
- package/cli/index.js +51 -7
- package/cli/installer.js +23 -6
- package/cli/mcp/antigravity.js +59 -0
- package/cli/mcp/index.js +2 -1
- package/cli/mcp/installer.js +26 -4
- package/cli/mcp/installer.spec.js +23 -1
- package/cli/mcp/manifests.js +251 -0
- package/cli/mcp/prompts.js +28 -0
- package/cli/mcp/resources.js +94 -90
- package/cli/mcp/server.js +13 -0
- package/cli/mcp/server.spec.js +213 -1
- package/cli/mcp/tools.js +252 -189
- package/cli/mcp/types.d.ts +37 -0
- package/cli/navigator-actions.js +23 -10
- package/cli/navigator-banner.js +2 -3
- package/cli/navigator-guide.js +73 -0
- package/cli/navigator-menu.js +20 -0
- package/cli/navigator.js +1 -0
- package/cli/patcher.d.ts +73 -0
- package/cli/patcher.js +249 -4
- package/cli/patcher.spec.js +148 -0
- package/cli/scaffold.js +7 -1
- package/cli/search-commands.js +10 -8
- package/cli/search-queries.js +8 -3
- package/cli/search-schema.js +1 -0
- package/cli/search.d.ts +16 -0
- package/cli/search.js +103 -11
- package/cli/silence-warnings.js +17 -0
- package/cli/verify.js +377 -0
- package/cli/verify.spec.js +98 -0
- package/package.json +3 -1
- package/cli/mcp.js +0 -326
package/README.md
CHANGED
|
@@ -34,6 +34,69 @@ starter-kit/
|
|
|
34
34
|
└── index.js # Interactive capsule generator
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Model Context Protocol (MCP) Server
|
|
40
|
+
|
|
41
|
+
Chemical X includes a high-performance, zero-dependency JSON-RPC 2.0 Stdio MCP server that connects directly to AI agent hosts (Cursor, Claude Desktop, Windsurf, Antigravity, VS Code).
|
|
42
|
+
|
|
43
|
+
### Automatic 1-Step Installation
|
|
44
|
+
|
|
45
|
+
Run the interactive installer in your workspace root:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx chemx install-mcp
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This automatically registers the Chemical X server in:
|
|
52
|
+
- `.cursor/mcp.json` (Cursor IDE)
|
|
53
|
+
- `.vscode/mcp.json` (VS Code)
|
|
54
|
+
- `~/.gemini/config/mcp_config.json` (Antigravity)
|
|
55
|
+
- Adds `"chemx:mcp": "chemx mcp"` to your `package.json` scripts
|
|
56
|
+
|
|
57
|
+
### Manual MCP Server Configuration
|
|
58
|
+
|
|
59
|
+
To configure manually in your MCP client settings (e.g. `claude_desktop_config.json` or `.cursor/mcp.json`):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"chemical-x": {
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "chemx", "mcp"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Available MCP Tools (10 Tools)
|
|
73
|
+
|
|
74
|
+
| Tool Name | Scope | Purpose |
|
|
75
|
+
| :--- | :--- | :--- |
|
|
76
|
+
| `chemx_q` | Discovery | AST search index query machine. Query symbols, capsules, props, and hooks with minimal token burn. |
|
|
77
|
+
| `chemx_query_patterns` | Discovery | Detect duplicated state machines, cloned UI layouts, and parallel hooks before decomposing monoliths. |
|
|
78
|
+
| `chemx_read` | Reading | Token-minified file reader. Extracts AST outlines, stripped comments, or symbol blocks (80%+ token savings). |
|
|
79
|
+
| `chemx_patch` | Editing | Surgically patch files with exact search and replace blocks without whole-file context dumps. |
|
|
80
|
+
| `chemx_check` | Linting | Verify a single file or capsule against molecular boundary rules (< 100L, 2-stage booleans, zero raw DOM). |
|
|
81
|
+
| `chemx_audit` | Quality | Run the full 7-Pillar Chemical X static AST audit. Returns health score, grade (A+ to F), and hazard list. |
|
|
82
|
+
| `chemx_audit_build` | Quality | Wrap build commands with silent execution and catalog compiler diagnostics into structured categories. |
|
|
83
|
+
| `chemx_autofix` | Remediation | Deterministically remediate safe violations (typography hyphens, markdown fences, AI slop comments). |
|
|
84
|
+
| `chemx_generate_capsule` | Scaffolding | Deterministically generate a crystalline capsule directory (component, controller, SCSS, types, index). |
|
|
85
|
+
| `chemx_get_refactor_prompt` | Prompting | Synthesize targeted refactoring prompts for Grade F critical hazards, hotspots, and slop artifacts. |
|
|
86
|
+
|
|
87
|
+
### Living Resources & Prompts
|
|
88
|
+
|
|
89
|
+
* **Resources**:
|
|
90
|
+
* `chemx://directives`: Mandatory Chemical X Molecular Architecture directives (AGENTS.md).
|
|
91
|
+
* `chemx://scorecard`: Live Molecular Health Index (MHI) grade, score, metrics, and active hazards.
|
|
92
|
+
* `chemx://blueprints/molecule`: Compliant molecule blueprint adhering to zero raw DOM standards.
|
|
93
|
+
* `chemx://blueprints/view-template`: Declarative 10-20 line Table-of-Contents view blueprint.
|
|
94
|
+
* **Prompts**:
|
|
95
|
+
* `chemx_remediate_hotspot`: Dynamically inspects candidate file AST metrics and generates targeted decomposition instructions.
|
|
96
|
+
* `chemx_harmonize_patterns`: Generates Pre-Split Pattern Discovery instructions to extract shared capsules.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
37
100
|
## Release Versioning: Minute-Precision CalVer
|
|
38
101
|
|
|
39
102
|
This package adheres to **Minute-Precision Calendar Versioning** (`YY.MM.DD-MMMM`):
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { computed } from 'vue';
|
|
2
2
|
import type { TabButtonProps, TabTone } from './types';
|
|
3
3
|
|
|
4
|
+
const TONE_CLASSES: Record<TabTone, string> = {
|
|
5
|
+
lime: 'm-tab-button--lime',
|
|
6
|
+
pink: 'm-tab-button--pink',
|
|
7
|
+
slate: 'm-tab-button--slate',
|
|
8
|
+
sky: 'm-tab-button--sky'
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
export function useTabButtonController(props: TabButtonProps, emit: (event: 'click' | 'select') => void) {
|
|
5
12
|
const isSelected = computed((): boolean => Boolean(props.isActive));
|
|
6
13
|
|
|
7
14
|
const resolveToneClass = (tone: TabTone = 'sky'): string => {
|
|
8
|
-
|
|
9
|
-
case 'lime':
|
|
10
|
-
return 'm-tab-button--lime';
|
|
11
|
-
case 'pink':
|
|
12
|
-
return 'm-tab-button--pink';
|
|
13
|
-
case 'slate':
|
|
14
|
-
return 'm-tab-button--slate';
|
|
15
|
-
case 'sky':
|
|
16
|
-
default:
|
|
17
|
-
return 'm-tab-button--sky';
|
|
18
|
-
}
|
|
15
|
+
return TONE_CLASSES[tone] || TONE_CLASSES.sky;
|
|
19
16
|
};
|
|
20
17
|
|
|
21
18
|
const stateClass = computed((): string => {
|
|
@@ -89,6 +89,29 @@ export const createAstVisitors = ({ relativePath, violations }) => {
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
|
|
92
|
+
IfStatement(astPath) {
|
|
93
|
+
const expr = astPath.node.test;
|
|
94
|
+
if (t.isLogicalExpression(expr)) {
|
|
95
|
+
const opCount = countLogicalOperators(expr);
|
|
96
|
+
const hasBinaryClauses = (t.isBinaryExpression(expr.left) || t.isLogicalExpression(expr.left)) &&
|
|
97
|
+
(t.isBinaryExpression(expr.right) || t.isLogicalExpression(expr.right));
|
|
98
|
+
if (opCount > 2 || (opCount >= 2 && hasBinaryClauses)) {
|
|
99
|
+
const line = resolveStartLine(expr, astPath.node, 1);
|
|
100
|
+
const meta = RULE_REGISTRY.CONTROL_FLOW_INLINE_BOOLEAN;
|
|
101
|
+
violations.push({
|
|
102
|
+
filePath: relativePath,
|
|
103
|
+
line,
|
|
104
|
+
column: expr.loc?.start.column || 1,
|
|
105
|
+
hazard: `Inline multi-clause boolean comparison in if statement (${opCount} operators). Decompose into 2-stage booleans per Directive 3.A.`,
|
|
106
|
+
rule: 'CONTROL_FLOW_INLINE_BOOLEAN',
|
|
107
|
+
severity: meta.severity,
|
|
108
|
+
pillar: meta.pillar,
|
|
109
|
+
directive: meta.directive
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
92
115
|
ConditionalExpression(astPath) {
|
|
93
116
|
if (t.isConditionalExpression(astPath.node.consequent) || t.isConditionalExpression(astPath.node.alternate)) {
|
|
94
117
|
const line = astPath.node.loc?.start.line || 1;
|
|
@@ -106,6 +129,56 @@ export const createAstVisitors = ({ relativePath, violations }) => {
|
|
|
106
129
|
}
|
|
107
130
|
},
|
|
108
131
|
|
|
132
|
+
SwitchStatement(astPath) {
|
|
133
|
+
const cases = astPath.node.cases || [];
|
|
134
|
+
if (cases.length < 3) return;
|
|
135
|
+
|
|
136
|
+
const isDispatchConsequent = (statements) => {
|
|
137
|
+
if (!statements || statements.length === 0) return false;
|
|
138
|
+
if (statements.length === 1) {
|
|
139
|
+
const s = statements[0];
|
|
140
|
+
if (t.isReturnStatement(s)) return true;
|
|
141
|
+
if (t.isBlockStatement(s)) {
|
|
142
|
+
return s.body.length === 1 && t.isReturnStatement(s.body[0]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (statements.length === 2) {
|
|
146
|
+
const [first, second] = statements;
|
|
147
|
+
if (t.isBreakStatement(second)) {
|
|
148
|
+
return t.isExpressionStatement(first) || t.isAssignmentExpression(first);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let dispatchCaseCount = 0;
|
|
155
|
+
let nonDefaultCount = 0;
|
|
156
|
+
|
|
157
|
+
for (const switchCase of cases) {
|
|
158
|
+
if (!switchCase.test) continue;
|
|
159
|
+
nonDefaultCount += 1;
|
|
160
|
+
if (isDispatchConsequent(switchCase.consequent)) {
|
|
161
|
+
dispatchCaseCount += 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isDispatchSmell = nonDefaultCount >= 3 && (dispatchCaseCount / nonDefaultCount) >= 0.7;
|
|
166
|
+
if (isDispatchSmell) {
|
|
167
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
168
|
+
const meta = RULE_REGISTRY.CONTROL_FLOW_DISPATCH_SWITCH;
|
|
169
|
+
violations.push({
|
|
170
|
+
filePath: relativePath,
|
|
171
|
+
line,
|
|
172
|
+
column: astPath.node.loc?.start.column || 1,
|
|
173
|
+
hazard: `Dispatch switch smell detected (${dispatchCaseCount} repetitive case branches)`,
|
|
174
|
+
rule: 'CONTROL_FLOW_DISPATCH_SWITCH',
|
|
175
|
+
severity: meta.severity,
|
|
176
|
+
pillar: meta.pillar,
|
|
177
|
+
directive: meta.directive
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
109
182
|
// Pillar 5: Design System & Inline Styles
|
|
110
183
|
JSXAttribute(astPath) {
|
|
111
184
|
const attrName = astPath.node.name?.name;
|
|
@@ -33,6 +33,55 @@ const FAKE_GREEN_PATTERNS = [
|
|
|
33
33
|
const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
|
|
34
34
|
const CLICKABLE_CONTAINER_PATTERN = new RegExp(['<', '(div|span)\\b', '(?![^>]*\\brole\\s*=\\s*[\'"](?:button|link|tab)[\'"])', '[^>]*\\b(?:@click|v-on:click)\\s*='].join(''), 'i');
|
|
35
35
|
|
|
36
|
+
const SENSITIVE_KEYWORDS = new Set([
|
|
37
|
+
'password', 'passwd', 'secret', 'token', 'apikey', 'api_key',
|
|
38
|
+
'authtoken', 'auth_token', 'accesstoken', 'access_token',
|
|
39
|
+
'refreshtoken', 'refresh_token', 'privatekey', 'private_key',
|
|
40
|
+
'creditcard', 'credit_card', 'ssn'
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const isSensitiveName = (name = '') => {
|
|
44
|
+
const normalized = String(name).toLowerCase();
|
|
45
|
+
if (SENSITIVE_KEYWORDS.has(normalized)) return true;
|
|
46
|
+
return /^(pass(word)?|token|secret|api[_-]?key)$/i.test(normalized);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const JAVASCRIPT_URL_REGEX = /^\s*javascript:/i;
|
|
50
|
+
const TABNABBING_TEMPLATE_PATTERN = /<a\b[^>]*\btarget\s*=\s*["']_blank["'][^>]*>/i;
|
|
51
|
+
const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*["']\s*javascript:/i;
|
|
52
|
+
|
|
53
|
+
const isSensitiveExpression = (node) => {
|
|
54
|
+
if (!node) return false;
|
|
55
|
+
if (t.isIdentifier(node)) {
|
|
56
|
+
return isSensitiveName(node.name);
|
|
57
|
+
}
|
|
58
|
+
if (t.isMemberExpression(node)) {
|
|
59
|
+
if (t.isIdentifier(node.property)) {
|
|
60
|
+
return isSensitiveName(node.property.name);
|
|
61
|
+
}
|
|
62
|
+
if (t.isStringLiteral(node.property)) {
|
|
63
|
+
return isSensitiveName(node.property.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (t.isObjectExpression(node)) {
|
|
67
|
+
return node.properties.some((prop) => {
|
|
68
|
+
if (t.isObjectProperty(prop)) {
|
|
69
|
+
const keyName = t.isIdentifier(prop.key)
|
|
70
|
+
? prop.key.name
|
|
71
|
+
: t.isStringLiteral(prop.key)
|
|
72
|
+
? prop.key.value
|
|
73
|
+
: '';
|
|
74
|
+
return isSensitiveName(keyName);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (t.isTemplateLiteral(node)) {
|
|
80
|
+
return node.expressions.some(isSensitiveExpression);
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
|
|
36
85
|
/**
|
|
37
86
|
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
38
87
|
*/
|
|
@@ -136,6 +185,41 @@ export const checkExtendedTextPatterns = (content, lines, relativePath, filePath
|
|
|
136
185
|
}
|
|
137
186
|
}
|
|
138
187
|
|
|
188
|
+
// Pillar 9: Reverse Tabnabbing in HTML / Vue templates
|
|
189
|
+
const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
|
|
190
|
+
if (hasTargetBlank) {
|
|
191
|
+
const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
|
|
192
|
+
if (!hasRelProtection) {
|
|
193
|
+
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
194
|
+
violations.push({
|
|
195
|
+
filePath: relativePath,
|
|
196
|
+
line: lineNum,
|
|
197
|
+
column: 1,
|
|
198
|
+
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
199
|
+
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
200
|
+
severity: meta.severity,
|
|
201
|
+
pillar: meta.pillar,
|
|
202
|
+
directive: meta.directive
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
|
|
208
|
+
const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
|
|
209
|
+
if (hasJsUrl) {
|
|
210
|
+
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
211
|
+
violations.push({
|
|
212
|
+
filePath: relativePath,
|
|
213
|
+
line: lineNum,
|
|
214
|
+
column: 1,
|
|
215
|
+
hazard: 'javascript: pseudo-protocol detected in template attribute',
|
|
216
|
+
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
217
|
+
severity: meta.severity,
|
|
218
|
+
pillar: meta.pillar,
|
|
219
|
+
directive: meta.directive
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
139
223
|
// Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
|
|
140
224
|
const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
|
|
141
225
|
if (hasClickableContainer) {
|
|
@@ -244,6 +328,49 @@ export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
|
244
328
|
});
|
|
245
329
|
}
|
|
246
330
|
}
|
|
331
|
+
|
|
332
|
+
// Pillar 9: External link target="_blank" without rel="noopener noreferrer"
|
|
333
|
+
const isAnchorElement = tagName === 'a';
|
|
334
|
+
if (isAnchorElement) {
|
|
335
|
+
const targetAttr = astPath.node.attributes.find((attr) => t.isJSXAttribute(attr) && attr.name?.name === 'target');
|
|
336
|
+
let isTargetBlank = false;
|
|
337
|
+
if (targetAttr) {
|
|
338
|
+
if (t.isStringLiteral(targetAttr.value)) {
|
|
339
|
+
isTargetBlank = targetAttr.value.value.trim().toLowerCase() === '_blank';
|
|
340
|
+
} else if (t.isJSXExpressionContainer(targetAttr.value) && t.isStringLiteral(targetAttr.value.expression)) {
|
|
341
|
+
isTargetBlank = targetAttr.value.expression.value.trim().toLowerCase() === '_blank';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isTargetBlank) {
|
|
346
|
+
const relAttr = astPath.node.attributes.find((attr) => t.isJSXAttribute(attr) && attr.name?.name === 'rel');
|
|
347
|
+
let hasRelProtection = false;
|
|
348
|
+
if (relAttr) {
|
|
349
|
+
if (t.isStringLiteral(relAttr.value)) {
|
|
350
|
+
const val = relAttr.value.value.toLowerCase();
|
|
351
|
+
hasRelProtection = val.includes('noopener') || val.includes('noreferrer');
|
|
352
|
+
} else if (t.isJSXExpressionContainer(relAttr.value) && t.isStringLiteral(relAttr.value.expression)) {
|
|
353
|
+
const val = relAttr.value.expression.value.toLowerCase();
|
|
354
|
+
hasRelProtection = val.includes('noopener') || val.includes('noreferrer');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!hasRelProtection) {
|
|
359
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
360
|
+
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
361
|
+
violations.push({
|
|
362
|
+
filePath: relativePath,
|
|
363
|
+
line,
|
|
364
|
+
column: astPath.node.loc?.start.column || 1,
|
|
365
|
+
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
366
|
+
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
367
|
+
severity: meta.severity,
|
|
368
|
+
pillar: meta.pillar,
|
|
369
|
+
directive: meta.directive
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
247
374
|
},
|
|
248
375
|
|
|
249
376
|
JSXAttribute(astPath) {
|
|
@@ -314,6 +441,118 @@ export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
|
314
441
|
}
|
|
315
442
|
}
|
|
316
443
|
}
|
|
444
|
+
|
|
445
|
+
// Pillar 9: javascript: pseudo-protocol URL in JSX
|
|
446
|
+
const isUrlAttribute = ['href', 'src', 'action', 'formAction'].includes(attrName);
|
|
447
|
+
if (isUrlAttribute) {
|
|
448
|
+
let isJsUrl = false;
|
|
449
|
+
const val = astPath.node.value;
|
|
450
|
+
if (t.isStringLiteral(val)) {
|
|
451
|
+
isJsUrl = JAVASCRIPT_URL_REGEX.test(val.value);
|
|
452
|
+
} else if (t.isJSXExpressionContainer(val)) {
|
|
453
|
+
const expr = val.expression;
|
|
454
|
+
if (t.isStringLiteral(expr)) {
|
|
455
|
+
isJsUrl = JAVASCRIPT_URL_REGEX.test(expr.value);
|
|
456
|
+
} else if (t.isTemplateLiteral(expr) && expr.quasis.length > 0) {
|
|
457
|
+
isJsUrl = JAVASCRIPT_URL_REGEX.test(expr.quasis[0].value.raw);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (isJsUrl) {
|
|
462
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
463
|
+
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
464
|
+
violations.push({
|
|
465
|
+
filePath: relativePath,
|
|
466
|
+
line,
|
|
467
|
+
column: astPath.node.loc?.start.column || 1,
|
|
468
|
+
hazard: `javascript: pseudo-protocol detected in ${attrName} attribute`,
|
|
469
|
+
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
470
|
+
severity: meta.severity,
|
|
471
|
+
pillar: meta.pillar,
|
|
472
|
+
directive: meta.directive
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
CallExpression(astPath) {
|
|
479
|
+
const callee = astPath.node.callee;
|
|
480
|
+
|
|
481
|
+
// Pillar 9: Dynamic code execution via eval()
|
|
482
|
+
if (t.isIdentifier(callee) && callee.name === 'eval') {
|
|
483
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
484
|
+
const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
|
|
485
|
+
violations.push({
|
|
486
|
+
filePath: relativePath,
|
|
487
|
+
line,
|
|
488
|
+
column: astPath.node.loc?.start.column || 1,
|
|
489
|
+
hazard: 'Dynamic code execution via eval() detected',
|
|
490
|
+
rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
|
|
491
|
+
severity: meta.severity,
|
|
492
|
+
pillar: meta.pillar,
|
|
493
|
+
directive: meta.directive
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Pillar 9: String-based code execution in setTimeout/setInterval
|
|
498
|
+
if (t.isIdentifier(callee) && (callee.name === 'setTimeout' || callee.name === 'setInterval')) {
|
|
499
|
+
const firstArg = astPath.node.arguments[0];
|
|
500
|
+
const isStringExecution = t.isStringLiteral(firstArg) || t.isTemplateLiteral(firstArg);
|
|
501
|
+
if (isStringExecution) {
|
|
502
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
503
|
+
const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
|
|
504
|
+
violations.push({
|
|
505
|
+
filePath: relativePath,
|
|
506
|
+
line,
|
|
507
|
+
column: astPath.node.loc?.start.column || 1,
|
|
508
|
+
hazard: `String-based dynamic code execution in ${callee.name} detected`,
|
|
509
|
+
rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
|
|
510
|
+
severity: meta.severity,
|
|
511
|
+
pillar: meta.pillar,
|
|
512
|
+
directive: meta.directive
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Pillar 9: Sensitive credential/token logging in console
|
|
518
|
+
const isConsoleCall = t.isMemberExpression(callee) && t.isIdentifier(callee.object) && callee.object.name === 'console';
|
|
519
|
+
if (isConsoleCall) {
|
|
520
|
+
const args = astPath.node.arguments || [];
|
|
521
|
+
const hasSensitiveArg = args.some(isSensitiveExpression);
|
|
522
|
+
if (hasSensitiveArg) {
|
|
523
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
524
|
+
const meta = RULE_REGISTRY.SECURITY_SENSITIVE_LOGGING;
|
|
525
|
+
violations.push({
|
|
526
|
+
filePath: relativePath,
|
|
527
|
+
line,
|
|
528
|
+
column: astPath.node.loc?.start.column || 1,
|
|
529
|
+
hazard: 'Sensitive variable (credential, token, or password) logged to console',
|
|
530
|
+
rule: 'SECURITY_SENSITIVE_LOGGING',
|
|
531
|
+
severity: meta.severity,
|
|
532
|
+
pillar: meta.pillar,
|
|
533
|
+
directive: meta.directive
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
NewExpression(astPath) {
|
|
540
|
+
// Pillar 9: Dynamic code execution via new Function()
|
|
541
|
+
const callee = astPath.node.callee;
|
|
542
|
+
if (t.isIdentifier(callee) && callee.name === 'Function') {
|
|
543
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
544
|
+
const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
|
|
545
|
+
violations.push({
|
|
546
|
+
filePath: relativePath,
|
|
547
|
+
line,
|
|
548
|
+
column: astPath.node.loc?.start.column || 1,
|
|
549
|
+
hazard: 'Dynamic code execution via new Function() constructor detected',
|
|
550
|
+
rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
|
|
551
|
+
severity: meta.severity,
|
|
552
|
+
pillar: meta.pillar,
|
|
553
|
+
directive: meta.directive
|
|
554
|
+
});
|
|
555
|
+
}
|
|
317
556
|
},
|
|
318
557
|
|
|
319
558
|
VariableDeclarator(astPath) {
|
|
@@ -36,31 +36,22 @@ export const groupViolationsBySeverity = (violations) => {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export const getSeverityBadge = (severity) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
case 'LOW':
|
|
47
|
-
return `${YELLOW}[LOW]${RESET}`;
|
|
48
|
-
default:
|
|
49
|
-
return `[${severity}]`;
|
|
50
|
-
}
|
|
39
|
+
const SEVERITY_BADGES = {
|
|
40
|
+
CRITICAL: `${RED}[CRITICAL]${RESET}`,
|
|
41
|
+
HIGH: `${ORANGE}[HIGH]${RESET}`,
|
|
42
|
+
MEDIUM: `${ORANGE}[MEDIUM]${RESET}`,
|
|
43
|
+
LOW: `${YELLOW}[LOW]${RESET}`
|
|
44
|
+
};
|
|
45
|
+
return SEVERITY_BADGES[severity] || `[${severity}]`;
|
|
51
46
|
};
|
|
52
47
|
|
|
53
48
|
export const getStatusBadge = (status) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return `${RED}✕ FAIL${RESET}`;
|
|
61
|
-
default:
|
|
62
|
-
return status;
|
|
63
|
-
}
|
|
49
|
+
const STATUS_BADGES = {
|
|
50
|
+
PASSED: `${GREEN}✔ PASS${RESET}`,
|
|
51
|
+
WARN: `${YELLOW}⚠ WARN${RESET}`,
|
|
52
|
+
FAILED: `${RED}✕ FAIL${RESET}`
|
|
53
|
+
};
|
|
54
|
+
return STATUS_BADGES[status] || status;
|
|
64
55
|
};
|
|
65
56
|
|
|
66
57
|
export const resolveIndividualPillarGrade = (pillarData) => {
|
|
@@ -36,6 +36,26 @@ export const RULE_REGISTRY = {
|
|
|
36
36
|
severity: 'CRITICAL',
|
|
37
37
|
directive: 'Extract hardcoded API keys and secrets into environment variables or secrets manager'
|
|
38
38
|
},
|
|
39
|
+
SECURITY_REVERSE_TABNABBING: {
|
|
40
|
+
pillar: PILLARS.PILLAR_9,
|
|
41
|
+
severity: 'MEDIUM',
|
|
42
|
+
directive: 'Add rel="noopener noreferrer" to external links with target="_blank" to prevent tab hijacking'
|
|
43
|
+
},
|
|
44
|
+
SECURITY_JAVASCRIPT_URL: {
|
|
45
|
+
pillar: PILLARS.PILLAR_9,
|
|
46
|
+
severity: 'CRITICAL',
|
|
47
|
+
directive: 'Do not use javascript: pseudo-protocol in links or action attributes; use event handlers'
|
|
48
|
+
},
|
|
49
|
+
SECURITY_DYNAMIC_CODE_EXECUTION: {
|
|
50
|
+
pillar: PILLARS.PILLAR_9,
|
|
51
|
+
severity: 'CRITICAL',
|
|
52
|
+
directive: 'Avoid eval(), new Function(), and string-based setTimeout/setInterval code execution'
|
|
53
|
+
},
|
|
54
|
+
SECURITY_SENSITIVE_LOGGING: {
|
|
55
|
+
pillar: PILLARS.PILLAR_9,
|
|
56
|
+
severity: 'HIGH',
|
|
57
|
+
directive: 'Never log sensitive credentials, tokens, or passwords to console diagnostics'
|
|
58
|
+
},
|
|
39
59
|
|
|
40
60
|
// Pillar 10: Testing Discipline
|
|
41
61
|
TEST_FAKE_GREEN: {
|
|
@@ -86,6 +106,11 @@ export const RULE_REGISTRY = {
|
|
|
86
106
|
severity: 'CRITICAL',
|
|
87
107
|
directive: 'Extract display states into computed descriptor objects or early returns'
|
|
88
108
|
},
|
|
109
|
+
CONTROL_FLOW_DISPATCH_SWITCH: {
|
|
110
|
+
pillar: PILLARS.PILLAR_2,
|
|
111
|
+
severity: 'MEDIUM',
|
|
112
|
+
directive: 'Replace repetitive dispatch switch with an O(1) keyed dictionary or method map (Directive 3.E)'
|
|
113
|
+
},
|
|
89
114
|
HOOK_SATURATION: {
|
|
90
115
|
pillar: PILLARS.PILLAR_3,
|
|
91
116
|
severity: 'HIGH',
|