@chemx/starter-kit 26.9.17-1100 → 26.9.17-1437
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/cli/audit/extended-text-patterns.js +218 -0
- package/cli/audit/extended-visitors.js +3 -217
- package/cli/build.js +21 -0
- package/cli/errors/catcher.js +80 -0
- package/cli/errors/errors.spec.js +163 -0
- package/cli/errors/formatter.js +94 -0
- package/cli/errors/index.js +52 -0
- package/cli/errors/publisher.js +76 -0
- package/cli/errors/sanitizer.js +50 -0
- package/cli/errors/storage.js +36 -0
- package/cli/errors/types.d.ts +42 -0
- package/cli/index.js +17 -3
- package/cli/installer-templates.js +1 -1
- package/cli/installer.js +11 -9
- package/cli/mcp/antigravity.js +17 -11
- package/cli/mcp/manifests.js +39 -0
- package/cli/mcp/server.js +4 -2
- package/cli/mcp/tools-search.js +237 -0
- package/cli/mcp/tools-team.js +143 -0
- package/cli/mcp/tools.js +104 -277
- package/cli/team/team-triage.js +10 -1
- package/cli/ui-template.js +1 -1
- package/package.json +5 -3
- package/scripts/pre-commit.sh +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RULE_REGISTRY } from './rules-registry.js';
|
|
4
|
+
import { toResultSync } from './rules-helpers.js';
|
|
5
|
+
import {
|
|
6
|
+
isComponentExtension,
|
|
7
|
+
isTemplateExtension,
|
|
8
|
+
isCommentLine
|
|
9
|
+
} from './rules-predicates.js';
|
|
10
|
+
|
|
11
|
+
const SECRET_PATTERNS = [
|
|
12
|
+
/(['"])(?:sk-[a-zA-Z0-9_-]{24,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z-_]{35})\1/
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const FAKE_GREEN_PATTERNS = [
|
|
16
|
+
/expect\s*\(\s*(true|false|1|0)\s*\)\s*\.\s*to(?:Be|Equal|StrictEqual)\s*\(\s*\1\s*\)/,
|
|
17
|
+
/assert\s*\.\s*(?:strictEqual|equal|deepEqual)\s*\(\s*(true|false|1|0)\s*,\s*\1\s*\)/,
|
|
18
|
+
/assert\s*\.\s*ok\s*\(\s*(?:true|1)\s*\)/
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
|
|
22
|
+
const CLICKABLE_CONTAINER_PATTERN = new RegExp(['<', '(div|span)\\b', '(?![^>]*\\brole\\s*=\\s*[\'"](?:button|link|tab)[\'"])', '[^>]*\\b(?:@click|v-on:click)\\s*='].join(''), 'i');
|
|
23
|
+
const TABNABBING_TEMPLATE_PATTERN = /<a\b[^>]*\btarget\s*=\s*["']_blank["'][^>]*>/i;
|
|
24
|
+
const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*["']\s*javascript:/i;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
28
|
+
*/
|
|
29
|
+
export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
30
|
+
const baseName = path.basename(filePath);
|
|
31
|
+
const ext = path.extname(filePath);
|
|
32
|
+
|
|
33
|
+
// Stage 1: Atomic Concept Declarations
|
|
34
|
+
const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
|
|
35
|
+
const isComponentExt = isComponentExtension(ext);
|
|
36
|
+
const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
|
|
37
|
+
|
|
38
|
+
// Stage 2: Unified Decision Variable & Early Guard Clause
|
|
39
|
+
const isCandidateComponent = isMoleculePath && isComponentExt;
|
|
40
|
+
const isMoleculeComponent = isCandidateComponent && !isTestOrSpecFile;
|
|
41
|
+
if (!isMoleculeComponent) return;
|
|
42
|
+
|
|
43
|
+
const dir = path.dirname(filePath);
|
|
44
|
+
const [siblings, dirError] = toResultSync(() => (fs.existsSync(dir) ? fs.readdirSync(dir) : []));
|
|
45
|
+
|
|
46
|
+
// Stage 1: Atomic Guard Check
|
|
47
|
+
const hasDirError = Boolean(dirError);
|
|
48
|
+
const hasSiblings = Boolean(siblings);
|
|
49
|
+
const canInspectSiblings = !hasDirError && hasSiblings;
|
|
50
|
+
if (!canInspectSiblings) return;
|
|
51
|
+
|
|
52
|
+
const isTestSibling = (fileName) => /\.(test|spec)\.[jt]sx?$/.test(fileName);
|
|
53
|
+
const hasCoLocatedTest = siblings.some(isTestSibling);
|
|
54
|
+
if (hasCoLocatedTest) return;
|
|
55
|
+
|
|
56
|
+
const meta = RULE_REGISTRY.TEST_MISSING_COLOCATED;
|
|
57
|
+
violations.push({
|
|
58
|
+
filePath: relativePath,
|
|
59
|
+
line: 1,
|
|
60
|
+
column: 1,
|
|
61
|
+
hazard: 'Molecule capsule missing co-located test file (*.spec.ts or *.test.ts)',
|
|
62
|
+
rule: 'TEST_MISSING_COLOCATED',
|
|
63
|
+
severity: meta.severity,
|
|
64
|
+
pillar: meta.pillar,
|
|
65
|
+
directive: meta.directive
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
|
|
70
|
+
checkMoleculeCoLocatedTest(filePath, relativePath, violations);
|
|
71
|
+
|
|
72
|
+
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
73
|
+
const ext = path.extname(filePath);
|
|
74
|
+
const isTemplateFile = isTemplateExtension(ext);
|
|
75
|
+
|
|
76
|
+
lines.forEach((lineText, idx) => {
|
|
77
|
+
const lineNum = idx + 1;
|
|
78
|
+
const trimmed = lineText.trim();
|
|
79
|
+
const isComment = isCommentLine(trimmed);
|
|
80
|
+
|
|
81
|
+
// Pillar 9: Hardcoded Secret Detection
|
|
82
|
+
const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
|
|
83
|
+
const canScanSecrets = !isComment && !hasEnvReference;
|
|
84
|
+
|
|
85
|
+
if (canScanSecrets) {
|
|
86
|
+
for (const pat of SECRET_PATTERNS) {
|
|
87
|
+
const matchesSecretPattern = pat.test(lineText);
|
|
88
|
+
const hasPlaceholder = lineText.includes('dummy') || lineText.includes('placeholder');
|
|
89
|
+
const isHardcodedSecret = matchesSecretPattern && !hasPlaceholder;
|
|
90
|
+
|
|
91
|
+
if (isHardcodedSecret) {
|
|
92
|
+
const meta = RULE_REGISTRY.SECURITY_HARDCODED_SECRET;
|
|
93
|
+
violations.push({
|
|
94
|
+
filePath: relativePath,
|
|
95
|
+
line: lineNum,
|
|
96
|
+
column: 1,
|
|
97
|
+
hazard: 'High-entropy API key or secret token detected in source code',
|
|
98
|
+
rule: 'SECURITY_HARDCODED_SECRET',
|
|
99
|
+
severity: meta.severity,
|
|
100
|
+
pillar: meta.pillar,
|
|
101
|
+
directive: meta.directive
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Pillar 9: Unsanitized v-html injection (Vue / HTML templates)
|
|
109
|
+
const hasVHtml = isTemplateFile && /\bv-html\s*=\s*["']/.test(lineText);
|
|
110
|
+
if (hasVHtml) {
|
|
111
|
+
const hasSanitizer = /(?:sanitize|DOMPurify|escapeHtml|filterXSS)/i.test(lineText);
|
|
112
|
+
const isUnsanitizedVHtml = !hasSanitizer;
|
|
113
|
+
|
|
114
|
+
if (isUnsanitizedVHtml) {
|
|
115
|
+
const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
|
|
116
|
+
violations.push({
|
|
117
|
+
filePath: relativePath,
|
|
118
|
+
line: lineNum,
|
|
119
|
+
column: 1,
|
|
120
|
+
hazard: 'Unsanitized HTML injection via v-html detected without DOMPurify',
|
|
121
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
122
|
+
severity: meta.severity,
|
|
123
|
+
pillar: meta.pillar,
|
|
124
|
+
directive: meta.directive
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Pillar 9: Reverse Tabnabbing in HTML / Vue templates
|
|
130
|
+
const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
|
|
131
|
+
if (hasTargetBlank) {
|
|
132
|
+
const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
|
|
133
|
+
if (!hasRelProtection) {
|
|
134
|
+
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
135
|
+
violations.push({
|
|
136
|
+
filePath: relativePath,
|
|
137
|
+
line: lineNum,
|
|
138
|
+
column: 1,
|
|
139
|
+
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
140
|
+
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
141
|
+
severity: meta.severity,
|
|
142
|
+
pillar: meta.pillar,
|
|
143
|
+
directive: meta.directive
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
|
|
149
|
+
const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
|
|
150
|
+
if (hasJsUrl) {
|
|
151
|
+
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
152
|
+
violations.push({
|
|
153
|
+
filePath: relativePath,
|
|
154
|
+
line: lineNum,
|
|
155
|
+
column: 1,
|
|
156
|
+
hazard: 'javascript: pseudo-protocol detected in template attribute',
|
|
157
|
+
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
158
|
+
severity: meta.severity,
|
|
159
|
+
pillar: meta.pillar,
|
|
160
|
+
directive: meta.directive
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
|
|
165
|
+
const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
|
|
166
|
+
if (hasClickableContainer) {
|
|
167
|
+
const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
|
|
168
|
+
violations.push({
|
|
169
|
+
filePath: relativePath,
|
|
170
|
+
line: lineNum,
|
|
171
|
+
column: 1,
|
|
172
|
+
hazard: 'Clickable generic container detected without native button/link semantics',
|
|
173
|
+
rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
|
|
174
|
+
severity: meta.severity,
|
|
175
|
+
pillar: meta.pillar,
|
|
176
|
+
directive: meta.directive
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Pillar 8: Missing alt attribute on img tags
|
|
181
|
+
const hasMissingAltImg = isTemplateFile && IMG_WITHOUT_ALT_PATTERN.test(lineText);
|
|
182
|
+
if (hasMissingAltImg) {
|
|
183
|
+
const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
|
|
184
|
+
violations.push({
|
|
185
|
+
filePath: relativePath,
|
|
186
|
+
line: lineNum,
|
|
187
|
+
column: 1,
|
|
188
|
+
hazard: 'Image element missing alt text attribute',
|
|
189
|
+
rule: 'A11Y_IMAGE_MISSING_ALT',
|
|
190
|
+
severity: meta.severity,
|
|
191
|
+
pillar: meta.pillar,
|
|
192
|
+
directive: meta.directive
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Pillar 10: Fake green tests (only in spec/test files)
|
|
197
|
+
const canCheckFakeGreen = isTestFile && !isComment;
|
|
198
|
+
if (canCheckFakeGreen) {
|
|
199
|
+
for (const pat of FAKE_GREEN_PATTERNS) {
|
|
200
|
+
const isFakeGreenMatch = pat.test(lineText);
|
|
201
|
+
if (isFakeGreenMatch) {
|
|
202
|
+
const meta = RULE_REGISTRY.TEST_FAKE_GREEN;
|
|
203
|
+
violations.push({
|
|
204
|
+
filePath: relativePath,
|
|
205
|
+
line: lineNum,
|
|
206
|
+
column: 1,
|
|
207
|
+
hazard: 'Fake green test assertion detected (asserting literal against itself)',
|
|
208
|
+
rule: 'TEST_FAKE_GREEN',
|
|
209
|
+
severity: meta.severity,
|
|
210
|
+
pillar: meta.pillar,
|
|
211
|
+
directive: meta.directive
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
};
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import * as t from '@babel/types';
|
|
4
2
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
isTemplateExtension,
|
|
9
|
-
isCommentLine
|
|
10
|
-
} from './rules-predicates.js';
|
|
3
|
+
import { checkExtendedTextPatterns } from './extended-text-patterns.js';
|
|
4
|
+
|
|
5
|
+
export { checkExtendedTextPatterns };
|
|
11
6
|
|
|
12
7
|
const BARE_BOOLEANS = new Set([
|
|
13
8
|
'loading', 'valid', 'active', 'visible', 'open', 'disabled',
|
|
@@ -20,19 +15,6 @@ const BARE_HANDLERS = new Set([
|
|
|
20
15
|
'reset', 'confirm', 'cancel', 'refresh', 'sync', 'send', 'pay'
|
|
21
16
|
]);
|
|
22
17
|
|
|
23
|
-
const SECRET_PATTERNS = [
|
|
24
|
-
/(['"])(?:sk-[a-zA-Z0-9_-]{24,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z-_]{35})\1/
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const FAKE_GREEN_PATTERNS = [
|
|
28
|
-
/expect\s*\(\s*(true|false|1|0)\s*\)\s*\.\s*to(?:Be|Equal|StrictEqual)\s*\(\s*\1\s*\)/,
|
|
29
|
-
/assert\s*\.\s*(?:strictEqual|equal|deepEqual)\s*\(\s*(true|false|1|0)\s*,\s*\1\s*\)/,
|
|
30
|
-
/assert\s*\.\s*ok\s*\(\s*(?:true|1)\s*\)/
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
|
|
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
|
-
|
|
36
18
|
const SENSITIVE_KEYWORDS = new Set([
|
|
37
19
|
'password', 'passwd', 'secret', 'token', 'apikey', 'api_key',
|
|
38
20
|
'authtoken', 'auth_token', 'accesstoken', 'access_token',
|
|
@@ -47,8 +29,6 @@ const isSensitiveName = (name = '') => {
|
|
|
47
29
|
};
|
|
48
30
|
|
|
49
31
|
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
32
|
|
|
53
33
|
const resolvePropertyKeyName = (key) => {
|
|
54
34
|
const isIdentifier = t.isIdentifier(key);
|
|
@@ -86,200 +66,6 @@ const isSensitiveExpression = (node) => {
|
|
|
86
66
|
return false;
|
|
87
67
|
};
|
|
88
68
|
|
|
89
|
-
/**
|
|
90
|
-
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
91
|
-
*/
|
|
92
|
-
const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
93
|
-
const baseName = path.basename(filePath);
|
|
94
|
-
const ext = path.extname(filePath);
|
|
95
|
-
|
|
96
|
-
// Stage 1: Atomic Concept Declarations
|
|
97
|
-
const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
|
|
98
|
-
const isComponentExt = isComponentExtension(ext);
|
|
99
|
-
const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
|
|
100
|
-
|
|
101
|
-
// Stage 2: Unified Decision Variable & Early Guard Clause
|
|
102
|
-
const isCandidateComponent = isMoleculePath && isComponentExt;
|
|
103
|
-
const isMoleculeComponent = isCandidateComponent && !isTestOrSpecFile;
|
|
104
|
-
if (!isMoleculeComponent) return;
|
|
105
|
-
|
|
106
|
-
const dir = path.dirname(filePath);
|
|
107
|
-
const [siblings, dirError] = toResultSync(() => (fs.existsSync(dir) ? fs.readdirSync(dir) : []));
|
|
108
|
-
|
|
109
|
-
// Stage 1: Atomic Guard Check
|
|
110
|
-
const hasDirError = Boolean(dirError);
|
|
111
|
-
const hasSiblings = Boolean(siblings);
|
|
112
|
-
const canInspectSiblings = !hasDirError && hasSiblings;
|
|
113
|
-
if (!canInspectSiblings) return;
|
|
114
|
-
|
|
115
|
-
const isTestSibling = (fileName) => /\.(test|spec)\.[jt]sx?$/.test(fileName);
|
|
116
|
-
const hasCoLocatedTest = siblings.some(isTestSibling);
|
|
117
|
-
if (hasCoLocatedTest) return;
|
|
118
|
-
|
|
119
|
-
const meta = RULE_REGISTRY.TEST_MISSING_COLOCATED;
|
|
120
|
-
violations.push({
|
|
121
|
-
filePath: relativePath,
|
|
122
|
-
line: 1,
|
|
123
|
-
column: 1,
|
|
124
|
-
hazard: 'Molecule capsule missing co-located test file (*.spec.ts or *.test.ts)',
|
|
125
|
-
rule: 'TEST_MISSING_COLOCATED',
|
|
126
|
-
severity: meta.severity,
|
|
127
|
-
pillar: meta.pillar,
|
|
128
|
-
directive: meta.directive
|
|
129
|
-
});
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
|
|
133
|
-
checkMoleculeCoLocatedTest(filePath, relativePath, violations);
|
|
134
|
-
|
|
135
|
-
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
136
|
-
const ext = path.extname(filePath);
|
|
137
|
-
const isTemplateFile = isTemplateExtension(ext);
|
|
138
|
-
|
|
139
|
-
lines.forEach((lineText, idx) => {
|
|
140
|
-
const lineNum = idx + 1;
|
|
141
|
-
const trimmed = lineText.trim();
|
|
142
|
-
const isComment = isCommentLine(trimmed);
|
|
143
|
-
|
|
144
|
-
// Pillar 9: Hardcoded Secret Detection
|
|
145
|
-
const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
|
|
146
|
-
const canScanSecrets = !isComment && !hasEnvReference;
|
|
147
|
-
|
|
148
|
-
if (canScanSecrets) {
|
|
149
|
-
for (const pat of SECRET_PATTERNS) {
|
|
150
|
-
const matchesSecretPattern = pat.test(lineText);
|
|
151
|
-
const hasPlaceholder = lineText.includes('dummy') || lineText.includes('placeholder');
|
|
152
|
-
const isHardcodedSecret = matchesSecretPattern && !hasPlaceholder;
|
|
153
|
-
|
|
154
|
-
if (isHardcodedSecret) {
|
|
155
|
-
const meta = RULE_REGISTRY.SECURITY_HARDCODED_SECRET;
|
|
156
|
-
violations.push({
|
|
157
|
-
filePath: relativePath,
|
|
158
|
-
line: lineNum,
|
|
159
|
-
column: 1,
|
|
160
|
-
hazard: 'High-entropy API key or secret token detected in source code',
|
|
161
|
-
rule: 'SECURITY_HARDCODED_SECRET',
|
|
162
|
-
severity: meta.severity,
|
|
163
|
-
pillar: meta.pillar,
|
|
164
|
-
directive: meta.directive
|
|
165
|
-
});
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Pillar 9: Unsanitized v-html injection (Vue / HTML templates)
|
|
172
|
-
const hasVHtml = isTemplateFile && /\bv-html\s*=\s*["']/.test(lineText);
|
|
173
|
-
if (hasVHtml) {
|
|
174
|
-
const hasSanitizer = /(?:sanitize|DOMPurify|escapeHtml|filterXSS)/i.test(lineText);
|
|
175
|
-
const isUnsanitizedVHtml = !hasSanitizer;
|
|
176
|
-
|
|
177
|
-
if (isUnsanitizedVHtml) {
|
|
178
|
-
const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
|
|
179
|
-
violations.push({
|
|
180
|
-
filePath: relativePath,
|
|
181
|
-
line: lineNum,
|
|
182
|
-
column: 1,
|
|
183
|
-
hazard: 'Unsanitized HTML injection via v-html detected without DOMPurify',
|
|
184
|
-
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
185
|
-
severity: meta.severity,
|
|
186
|
-
pillar: meta.pillar,
|
|
187
|
-
directive: meta.directive
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Pillar 9: Reverse Tabnabbing in HTML / Vue templates
|
|
193
|
-
const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
|
|
194
|
-
if (hasTargetBlank) {
|
|
195
|
-
const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
|
|
196
|
-
if (!hasRelProtection) {
|
|
197
|
-
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
198
|
-
violations.push({
|
|
199
|
-
filePath: relativePath,
|
|
200
|
-
line: lineNum,
|
|
201
|
-
column: 1,
|
|
202
|
-
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
203
|
-
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
204
|
-
severity: meta.severity,
|
|
205
|
-
pillar: meta.pillar,
|
|
206
|
-
directive: meta.directive
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
|
|
212
|
-
const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
|
|
213
|
-
if (hasJsUrl) {
|
|
214
|
-
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
215
|
-
violations.push({
|
|
216
|
-
filePath: relativePath,
|
|
217
|
-
line: lineNum,
|
|
218
|
-
column: 1,
|
|
219
|
-
hazard: 'javascript: pseudo-protocol detected in template attribute',
|
|
220
|
-
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
221
|
-
severity: meta.severity,
|
|
222
|
-
pillar: meta.pillar,
|
|
223
|
-
directive: meta.directive
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
|
|
228
|
-
const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
|
|
229
|
-
if (hasClickableContainer) {
|
|
230
|
-
const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
|
|
231
|
-
violations.push({
|
|
232
|
-
filePath: relativePath,
|
|
233
|
-
line: lineNum,
|
|
234
|
-
column: 1,
|
|
235
|
-
hazard: 'Clickable generic container detected without native button/link semantics',
|
|
236
|
-
rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
|
|
237
|
-
severity: meta.severity,
|
|
238
|
-
pillar: meta.pillar,
|
|
239
|
-
directive: meta.directive
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Pillar 8: Missing alt attribute on img tags
|
|
244
|
-
const hasMissingAltImg = isTemplateFile && IMG_WITHOUT_ALT_PATTERN.test(lineText);
|
|
245
|
-
if (hasMissingAltImg) {
|
|
246
|
-
const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
|
|
247
|
-
violations.push({
|
|
248
|
-
filePath: relativePath,
|
|
249
|
-
line: lineNum,
|
|
250
|
-
column: 1,
|
|
251
|
-
hazard: 'Image element missing alt text attribute',
|
|
252
|
-
rule: 'A11Y_IMAGE_MISSING_ALT',
|
|
253
|
-
severity: meta.severity,
|
|
254
|
-
pillar: meta.pillar,
|
|
255
|
-
directive: meta.directive
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Pillar 10: Fake green tests (only in spec/test files)
|
|
260
|
-
const canCheckFakeGreen = isTestFile && !isComment;
|
|
261
|
-
if (canCheckFakeGreen) {
|
|
262
|
-
for (const pat of FAKE_GREEN_PATTERNS) {
|
|
263
|
-
const isFakeGreenMatch = pat.test(lineText);
|
|
264
|
-
if (isFakeGreenMatch) {
|
|
265
|
-
const meta = RULE_REGISTRY.TEST_FAKE_GREEN;
|
|
266
|
-
violations.push({
|
|
267
|
-
filePath: relativePath,
|
|
268
|
-
line: lineNum,
|
|
269
|
-
column: 1,
|
|
270
|
-
hazard: 'Fake green test assertion detected (asserting literal against itself)',
|
|
271
|
-
rule: 'TEST_FAKE_GREEN',
|
|
272
|
-
severity: meta.severity,
|
|
273
|
-
pillar: meta.pillar,
|
|
274
|
-
directive: meta.directive
|
|
275
|
-
});
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
};
|
|
282
|
-
|
|
283
69
|
export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
284
70
|
return {
|
|
285
71
|
JSXOpeningElement(astPath) {
|
package/cli/build.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseBuildOutput } from './build/parser.js';
|
|
|
4
4
|
import { groupBuildDiagnostics } from './build/grouper.js';
|
|
5
5
|
import { formatTerminalBuildReport, formatJsonBuildReport } from './build/reporter.js';
|
|
6
6
|
import { ANSI } from './theme.js';
|
|
7
|
+
import { handleError } from './errors/index.js';
|
|
7
8
|
|
|
8
9
|
const IGNORED_COMMAND_TOKENS = new Set(['build', 'run', 'wrap']);
|
|
9
10
|
|
|
@@ -53,6 +54,26 @@ export const runBuildAudit = async (rawArgs = [], isCli = false, options = {}) =
|
|
|
53
54
|
process.stdout.write(terminalOutput);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
const isFailedBuild = report.exitCode !== 0;
|
|
58
|
+
const hasIssueFlag = rawArgs.includes('--prep-issue') || rawArgs.includes('--post-issue');
|
|
59
|
+
const shouldHandleError = isFailedBuild && (hasIssueFlag || options.postIssue || (isCli && !isJson));
|
|
60
|
+
|
|
61
|
+
if (shouldHandleError) {
|
|
62
|
+
const errorDetails = `Build command failed with exit code ${report.exitCode}: ${command}`;
|
|
63
|
+
await handleError(new Error(errorDetails), {
|
|
64
|
+
cwd,
|
|
65
|
+
command,
|
|
66
|
+
exitCode: report.exitCode,
|
|
67
|
+
autoPost: rawArgs.includes('--post-issue') || options.postIssue,
|
|
68
|
+
silent: isSilent || isJson,
|
|
69
|
+
context: {
|
|
70
|
+
totalErrors: report.totalErrors,
|
|
71
|
+
totalWarnings: report.totalWarnings,
|
|
72
|
+
categories: report.categories
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
if (isCli) {
|
|
57
78
|
process.exit(report.exitCode);
|
|
58
79
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { copyToClipboard } from '../audit/social-git.js';
|
|
2
|
+
import { formatIssueContent } from './formatter.js';
|
|
3
|
+
import { publishIssue } from './publisher.js';
|
|
4
|
+
import { openBrowser, confirmAction } from '../terminal.js';
|
|
5
|
+
import { resolveTargetIssuesRepo, saveIssueArtifact } from './storage.js';
|
|
6
|
+
|
|
7
|
+
export { resolveTargetIssuesRepo, saveIssueArtifact };
|
|
8
|
+
|
|
9
|
+
const promptUserToPublish = async (targetRepo, issue) => {
|
|
10
|
+
const confirmed = await confirmAction('Post this error report directly to GitHub Issues?', 'Post Issue', 'Skip', false);
|
|
11
|
+
if (!confirmed) {
|
|
12
|
+
return { success: false, url: null, issueNumber: null, error: null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const publishResult = await publishIssue(targetRepo, issue.title, issue.body, issue.labels);
|
|
16
|
+
if (publishResult.success) {
|
|
17
|
+
process.stdout.write(`\x1b[32m✔ Issue published: ${publishResult.url}\x1b[0m\n`);
|
|
18
|
+
openBrowser(publishResult.url);
|
|
19
|
+
} else {
|
|
20
|
+
copyToClipboard(issue.body);
|
|
21
|
+
process.stdout.write(`\x1b[33m⚠ Publish failed. Markdown copied to clipboard. Opening browser...\x1b[0m\n`);
|
|
22
|
+
openBrowser(issue.webUrl);
|
|
23
|
+
}
|
|
24
|
+
return publishResult;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const handleError = async (err, options = {}) => {
|
|
28
|
+
const cwd = options.cwd || process.cwd();
|
|
29
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
30
|
+
const targetRepo = resolveTargetIssuesRepo(options, cwd);
|
|
31
|
+
|
|
32
|
+
const rawCmd = options.command || process.argv.slice(2).join(' ');
|
|
33
|
+
const command = rawCmd || 'chemx';
|
|
34
|
+
|
|
35
|
+
const report = {
|
|
36
|
+
message: errorObj.message,
|
|
37
|
+
name: errorObj.name,
|
|
38
|
+
stack: errorObj.stack || '',
|
|
39
|
+
command,
|
|
40
|
+
cwd,
|
|
41
|
+
exitCode: options.exitCode ?? 1,
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
nodeVersion: process.version,
|
|
44
|
+
platform: `${process.platform}-${process.arch}`,
|
|
45
|
+
chemxVersion: options.chemxVersion || '26.9.17',
|
|
46
|
+
context: options.context || {}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const issue = formatIssueContent(report, targetRepo, options.labels);
|
|
50
|
+
const savedPath = saveIssueArtifact(cwd, issue);
|
|
51
|
+
|
|
52
|
+
const hasToken = Boolean(process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
|
|
53
|
+
const isCiEnv = Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
|
|
54
|
+
const isExplicitAutoPost = Boolean(options.autoPost || process.env.CHEMX_AUTO_POST_ISSUES === 'true');
|
|
55
|
+
const isCiAutoPost = isCiEnv && hasToken && options.autoPost !== false;
|
|
56
|
+
const shouldAutoPost = isExplicitAutoPost || isCiAutoPost;
|
|
57
|
+
|
|
58
|
+
let publishResult = { success: false, url: null, issueNumber: null, error: null };
|
|
59
|
+
|
|
60
|
+
if (shouldAutoPost) {
|
|
61
|
+
publishResult = await publishIssue(targetRepo, issue.title, issue.body, issue.labels);
|
|
62
|
+
if (publishResult.success && !options.silent) {
|
|
63
|
+
process.stdout.write(`\n\x1b[32m✔ Issue automatically created: ${publishResult.url}\x1b[0m\n`);
|
|
64
|
+
}
|
|
65
|
+
} else if (!options.silent) {
|
|
66
|
+
const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
67
|
+
const isInteractive = isTty && !isCiEnv;
|
|
68
|
+
process.stderr.write(`\n\x1b[31m✕ Command Failed: ${report.message}\x1b[0m\n`);
|
|
69
|
+
process.stderr.write(` \x1b[33m• Prepped Issue:\x1b[0m ${issue.webUrl}\n`);
|
|
70
|
+
if (savedPath) {
|
|
71
|
+
process.stderr.write(` \x1b[33m• Local Report:\x1b[0m ${savedPath}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isInteractive) {
|
|
75
|
+
publishResult = await promptUserToPublish(targetRepo, issue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { report, issue, publishResult, savedPath };
|
|
80
|
+
};
|