guardrail-ship 1.0.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/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-implementation.d.ts +1 -0
- package/dist/mock-implementation.d.ts.map +1 -0
- package/dist/mock-implementation.js +2 -0
- package/dist/mock-implementation.js.map +1 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
- package/dist/mockproof/import-graph-scanner.d.ts +93 -0
- package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
- package/dist/mockproof/import-graph-scanner.js +411 -0
- package/dist/mockproof/import-graph-scanner.js.map +1 -0
- package/dist/mockproof/index.d.ts +10 -0
- package/dist/mockproof/index.d.ts.map +1 -0
- package/dist/mockproof/index.js +10 -0
- package/dist/mockproof/index.js.map +1 -0
- package/dist/reality-mode/auth-enforcer.d.ts +13 -0
- package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
- package/dist/reality-mode/auth-enforcer.js +90 -0
- package/dist/reality-mode/auth-enforcer.js.map +1 -0
- package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
- package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
- package/dist/reality-mode/explorer/critical-flows.js +463 -0
- package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
- package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
- package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
- package/dist/reality-mode/explorer/flow-parser.js +250 -0
- package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
- package/dist/reality-mode/explorer/index.d.ts +11 -0
- package/dist/reality-mode/explorer/index.d.ts.map +1 -0
- package/dist/reality-mode/explorer/index.js +11 -0
- package/dist/reality-mode/explorer/index.js.map +1 -0
- package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
- package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
- package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
- package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
- package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
- package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
- package/dist/reality-mode/explorer/surface-discovery.js +357 -0
- package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
- package/dist/reality-mode/explorer/types.d.ts +275 -0
- package/dist/reality-mode/explorer/types.d.ts.map +1 -0
- package/dist/reality-mode/explorer/types.js +8 -0
- package/dist/reality-mode/explorer/types.js.map +1 -0
- package/dist/reality-mode/fake-success-detector.d.ts +10 -0
- package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
- package/dist/reality-mode/fake-success-detector.js +76 -0
- package/dist/reality-mode/fake-success-detector.js.map +1 -0
- package/dist/reality-mode/index.d.ts +14 -0
- package/dist/reality-mode/index.d.ts.map +1 -0
- package/dist/reality-mode/index.js +14 -0
- package/dist/reality-mode/index.js.map +1 -0
- package/dist/reality-mode/reality-scanner.d.ts +48 -0
- package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
- package/dist/reality-mode/reality-scanner.js +516 -0
- package/dist/reality-mode/reality-scanner.js.map +1 -0
- package/dist/reality-mode/report-generator.d.ts +11 -0
- package/dist/reality-mode/report-generator.d.ts.map +1 -0
- package/dist/reality-mode/report-generator.js +233 -0
- package/dist/reality-mode/report-generator.js.map +1 -0
- package/dist/reality-mode/traffic-classifier.d.ts +14 -0
- package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
- package/dist/reality-mode/traffic-classifier.js +131 -0
- package/dist/reality-mode/traffic-classifier.js.map +1 -0
- package/dist/reality-mode/types.d.ts +90 -0
- package/dist/reality-mode/types.d.ts.map +1 -0
- package/dist/reality-mode/types.js +2 -0
- package/dist/reality-mode/types.js.map +1 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
- package/dist/ship-badge/index.d.ts +9 -0
- package/dist/ship-badge/index.d.ts.map +1 -0
- package/dist/ship-badge/index.js +9 -0
- package/dist/ship-badge/index.js.map +1 -0
- package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
- package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
- package/dist/ship-badge/ship-badge-generator.js +681 -0
- package/dist/ship-badge/ship-badge-generator.js.map +1 -0
- package/package.json +20 -0
- package/src/index.ts +7 -0
- package/src/mock-implementation.ts +0 -0
- package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
- package/src/mockproof/import-graph-scanner.d.ts +93 -0
- package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
- package/src/mockproof/import-graph-scanner.js +482 -0
- package/src/mockproof/import-graph-scanner.ts +540 -0
- package/src/mockproof/index.ts +18 -0
- package/src/reality-mode/auth-enforcer.ts +97 -0
- package/src/reality-mode/explorer/critical-flows.ts +504 -0
- package/src/reality-mode/explorer/flow-parser.ts +293 -0
- package/src/reality-mode/explorer/index.ts +22 -0
- package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
- package/src/reality-mode/explorer/surface-discovery.ts +498 -0
- package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
- package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
- package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
- package/src/reality-mode/explorer/templates/github-action.yml +132 -0
- package/src/reality-mode/explorer/types.ts +356 -0
- package/src/reality-mode/fake-success-detector.ts +89 -0
- package/src/reality-mode/index.ts +19 -0
- package/src/reality-mode/reality-scanner.d.ts +123 -0
- package/src/reality-mode/reality-scanner.d.ts.map +1 -0
- package/src/reality-mode/reality-scanner.js +526 -0
- package/src/reality-mode/reality-scanner.ts +576 -0
- package/src/reality-mode/report-generator.ts +253 -0
- package/src/reality-mode/traffic-classifier.ts +169 -0
- package/src/reality-mode/types.ts +95 -0
- package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
- package/src/ship-badge/index.ts +16 -0
- package/src/ship-badge/ship-badge-generator.d.ts +136 -0
- package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
- package/src/ship-badge/ship-badge-generator.js +779 -0
- package/src/ship-badge/ship-badge-generator.ts +873 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MockProof Build Gate - Import Graph Scanner
|
|
4
|
+
*
|
|
5
|
+
* Scans the import graph from production entrypoints to detect
|
|
6
|
+
* banned imports (MockProvider, useMock, mock-context, localhost, etc.)
|
|
7
|
+
* that would ship to production.
|
|
8
|
+
*
|
|
9
|
+
* This is the "one rule, one red line" feature that vibecoders love.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding =
|
|
12
|
+
(this && this.__createBinding) ||
|
|
13
|
+
(Object.create
|
|
14
|
+
? function (o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (
|
|
18
|
+
!desc ||
|
|
19
|
+
("get" in desc ? !m.__esModule : desc.writable || desc.configurable)
|
|
20
|
+
) {
|
|
21
|
+
desc = {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
get: function () {
|
|
24
|
+
return m[k];
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}
|
|
30
|
+
: function (o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
});
|
|
34
|
+
var __setModuleDefault =
|
|
35
|
+
(this && this.__setModuleDefault) ||
|
|
36
|
+
(Object.create
|
|
37
|
+
? function (o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}
|
|
40
|
+
: function (o, v) {
|
|
41
|
+
o["default"] = v;
|
|
42
|
+
});
|
|
43
|
+
var __importStar =
|
|
44
|
+
(this && this.__importStar) ||
|
|
45
|
+
(function () {
|
|
46
|
+
var ownKeys = function (o) {
|
|
47
|
+
ownKeys =
|
|
48
|
+
Object.getOwnPropertyNames ||
|
|
49
|
+
function (o) {
|
|
50
|
+
var ar = [];
|
|
51
|
+
for (var k in o)
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
53
|
+
return ar;
|
|
54
|
+
};
|
|
55
|
+
return ownKeys(o);
|
|
56
|
+
};
|
|
57
|
+
return function (mod) {
|
|
58
|
+
if (mod && mod.__esModule) return mod;
|
|
59
|
+
var result = {};
|
|
60
|
+
if (mod != null)
|
|
61
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++)
|
|
62
|
+
if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
63
|
+
__setModuleDefault(result, mod);
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
})();
|
|
67
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68
|
+
exports.importGraphScanner = exports.ImportGraphScanner = void 0;
|
|
69
|
+
const fs = __importStar(require("fs"));
|
|
70
|
+
const path = __importStar(require("path"));
|
|
71
|
+
const DEFAULT_BANNED_IMPORTS = [
|
|
72
|
+
{
|
|
73
|
+
pattern: "MockProvider",
|
|
74
|
+
message: "MockProvider should not be reachable from production entrypoints",
|
|
75
|
+
isRegex: false,
|
|
76
|
+
allowedIn: [
|
|
77
|
+
"**/__tests__/**",
|
|
78
|
+
"**/test/**",
|
|
79
|
+
"**/stories/**",
|
|
80
|
+
"**/landing/**",
|
|
81
|
+
"**/*.test.*",
|
|
82
|
+
"**/*.spec.*",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
pattern: "useMock",
|
|
87
|
+
message: "useMock hook should not be reachable from production entrypoints",
|
|
88
|
+
isRegex: false,
|
|
89
|
+
allowedIn: ["**/__tests__/**", "**/test/**", "**/stories/**"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: "mock-context",
|
|
93
|
+
message: "mock-context imports are not allowed in production",
|
|
94
|
+
isRegex: false,
|
|
95
|
+
allowedIn: ["**/__tests__/**", "**/test/**"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
pattern: "localhost:\\d+",
|
|
99
|
+
message: "Hardcoded localhost URLs will break in production",
|
|
100
|
+
isRegex: true,
|
|
101
|
+
allowedIn: [
|
|
102
|
+
"**/*.test.*",
|
|
103
|
+
"**/*.spec.*",
|
|
104
|
+
"**/docs/**",
|
|
105
|
+
"**/.env.example",
|
|
106
|
+
"**/e2e/**",
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
pattern: "jsonplaceholder\\.typicode\\.com",
|
|
111
|
+
message: "JSONPlaceholder is a mock API - not for production",
|
|
112
|
+
isRegex: true,
|
|
113
|
+
allowedIn: ["**/__tests__/**", "**/docs/**", "**/examples/**"],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: "\\.ngrok\\.io",
|
|
117
|
+
message: "ngrok URLs are temporary and will break in production",
|
|
118
|
+
isRegex: true,
|
|
119
|
+
allowedIn: ["**/__tests__/**", "**/docs/**"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
pattern: "sk_test_|pk_test_",
|
|
123
|
+
message: "Test API keys should not be in production code",
|
|
124
|
+
isRegex: true,
|
|
125
|
+
allowedIn: ["**/__tests__/**", "**/docs/**", "**/*.example"],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
pattern: "demo_|inv_demo|fake_",
|
|
129
|
+
message: "Demo/fake identifiers detected - not for production",
|
|
130
|
+
isRegex: true,
|
|
131
|
+
allowedIn: ["**/__tests__/**", "**/docs/**"],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: "DEMO_MODE|MOCK_MODE|USE_MOCKS",
|
|
135
|
+
message: "Feature flags for mock mode detected",
|
|
136
|
+
isRegex: true,
|
|
137
|
+
allowedIn: ["**/__tests__/**", "**/.env.example"],
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
const DEFAULT_CONFIG = {
|
|
141
|
+
entrypoints: [
|
|
142
|
+
"src/app/layout.tsx",
|
|
143
|
+
"src/app/page.tsx",
|
|
144
|
+
"src/pages/_app.tsx",
|
|
145
|
+
"src/pages/index.tsx",
|
|
146
|
+
"src/index.tsx",
|
|
147
|
+
"src/main.tsx",
|
|
148
|
+
"apps/web-ui/src/app/layout.tsx",
|
|
149
|
+
"apps/web-ui/src/app/page.tsx",
|
|
150
|
+
"apps/api/src/index.ts",
|
|
151
|
+
"apps/api/src/main.ts",
|
|
152
|
+
],
|
|
153
|
+
bannedImports: DEFAULT_BANNED_IMPORTS,
|
|
154
|
+
excludeDirs: [
|
|
155
|
+
"node_modules",
|
|
156
|
+
".git",
|
|
157
|
+
".next",
|
|
158
|
+
"dist",
|
|
159
|
+
"build",
|
|
160
|
+
"coverage",
|
|
161
|
+
"__tests__",
|
|
162
|
+
"__mocks__",
|
|
163
|
+
"test",
|
|
164
|
+
"tests",
|
|
165
|
+
"e2e",
|
|
166
|
+
"stories",
|
|
167
|
+
".storybook",
|
|
168
|
+
],
|
|
169
|
+
includeExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
170
|
+
};
|
|
171
|
+
class ImportGraphScanner {
|
|
172
|
+
config;
|
|
173
|
+
importGraph = new Map();
|
|
174
|
+
fileContents = new Map();
|
|
175
|
+
constructor(config = {}) {
|
|
176
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Scan a project for banned imports reachable from production entrypoints
|
|
180
|
+
*/
|
|
181
|
+
async scan(projectPath) {
|
|
182
|
+
this.importGraph.clear();
|
|
183
|
+
this.fileContents.clear();
|
|
184
|
+
// 1. Find all source files
|
|
185
|
+
const files = await this.findSourceFiles(projectPath);
|
|
186
|
+
// 2. Build import graph
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
await this.parseFile(file, projectPath);
|
|
189
|
+
}
|
|
190
|
+
// 3. Find valid entrypoints
|
|
191
|
+
const validEntrypoints = this.config.entrypoints
|
|
192
|
+
.map((ep) => path.join(projectPath, ep))
|
|
193
|
+
.filter((ep) => fs.existsSync(ep));
|
|
194
|
+
// 4. Trace from entrypoints to find violations
|
|
195
|
+
const violations = [];
|
|
196
|
+
for (const entrypoint of validEntrypoints) {
|
|
197
|
+
const entrypointViolations = this.traceFromEntrypoint(
|
|
198
|
+
entrypoint,
|
|
199
|
+
projectPath,
|
|
200
|
+
);
|
|
201
|
+
violations.push(...entrypointViolations);
|
|
202
|
+
}
|
|
203
|
+
// 5. Build result
|
|
204
|
+
const uniqueBanned = new Set(violations.map((v) => v.bannedImport));
|
|
205
|
+
const affectedEntrypoints = new Set(violations.map((v) => v.entrypoint));
|
|
206
|
+
return {
|
|
207
|
+
verdict: violations.length > 0 ? "fail" : "pass",
|
|
208
|
+
violations,
|
|
209
|
+
scannedFiles: this.importGraph.size,
|
|
210
|
+
entrypoints: validEntrypoints.map((ep) => path.relative(projectPath, ep)),
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
summary: {
|
|
213
|
+
totalViolations: violations.length,
|
|
214
|
+
uniqueBannedImports: uniqueBanned.size,
|
|
215
|
+
affectedEntrypoints: affectedEntrypoints.size,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Find all source files in the project
|
|
221
|
+
*/
|
|
222
|
+
async findSourceFiles(projectPath) {
|
|
223
|
+
const files = [];
|
|
224
|
+
const walk = async (dir) => {
|
|
225
|
+
try {
|
|
226
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const fullPath = path.join(dir, entry.name);
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
// Skip excluded directories
|
|
231
|
+
if (
|
|
232
|
+
!this.config.excludeDirs.includes(entry.name) &&
|
|
233
|
+
!entry.name.startsWith(".")
|
|
234
|
+
) {
|
|
235
|
+
await walk(fullPath);
|
|
236
|
+
}
|
|
237
|
+
} else if (entry.isFile()) {
|
|
238
|
+
const ext = path.extname(entry.name);
|
|
239
|
+
if (this.config.includeExtensions.includes(ext)) {
|
|
240
|
+
files.push(fullPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// Skip directories that can't be read
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
await walk(projectPath);
|
|
249
|
+
return files;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Parse a file and extract its imports
|
|
253
|
+
*/
|
|
254
|
+
async parseFile(filePath, projectPath) {
|
|
255
|
+
try {
|
|
256
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
257
|
+
this.fileContents.set(filePath, content);
|
|
258
|
+
const imports = this.extractImports(content, filePath, projectPath);
|
|
259
|
+
const node = {
|
|
260
|
+
file: filePath,
|
|
261
|
+
imports,
|
|
262
|
+
importedBy: [],
|
|
263
|
+
};
|
|
264
|
+
this.importGraph.set(filePath, node);
|
|
265
|
+
// Update importedBy for resolved imports
|
|
266
|
+
for (const imp of imports) {
|
|
267
|
+
const resolved = this.resolveImport(imp, filePath, projectPath);
|
|
268
|
+
if (resolved && this.importGraph.has(resolved)) {
|
|
269
|
+
this.importGraph.get(resolved).importedBy.push(filePath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Skip files that can't be read
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Extract import statements from file content
|
|
278
|
+
*/
|
|
279
|
+
extractImports(content, filePath, projectPath) {
|
|
280
|
+
const imports = [];
|
|
281
|
+
// ES6 imports: import X from 'Y', import { X } from 'Y', import 'Y'
|
|
282
|
+
const es6ImportRegex =
|
|
283
|
+
/import\s+(?:(?:\{[^}]*\}|[\w*]+(?:\s+as\s+\w+)?|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
284
|
+
let match;
|
|
285
|
+
while ((match = es6ImportRegex.exec(content)) !== null) {
|
|
286
|
+
imports.push(match[1]);
|
|
287
|
+
}
|
|
288
|
+
// Dynamic imports: import('Y')
|
|
289
|
+
const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
290
|
+
while ((match = dynamicImportRegex.exec(content)) !== null) {
|
|
291
|
+
imports.push(match[1]);
|
|
292
|
+
}
|
|
293
|
+
// CommonJS requires: require('Y')
|
|
294
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
295
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
296
|
+
imports.push(match[1]);
|
|
297
|
+
}
|
|
298
|
+
return imports;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Resolve an import path to an absolute file path
|
|
302
|
+
*/
|
|
303
|
+
resolveImport(importPath, fromFile, projectPath) {
|
|
304
|
+
// Skip node_modules imports
|
|
305
|
+
if (
|
|
306
|
+
!importPath.startsWith(".") &&
|
|
307
|
+
!importPath.startsWith("/") &&
|
|
308
|
+
!importPath.startsWith("@/")
|
|
309
|
+
) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const fromDir = path.dirname(fromFile);
|
|
313
|
+
let resolved;
|
|
314
|
+
if (importPath.startsWith("@/")) {
|
|
315
|
+
// Alias resolution (common in Next.js/React projects)
|
|
316
|
+
resolved = path.join(projectPath, "src", importPath.slice(2));
|
|
317
|
+
} else {
|
|
318
|
+
resolved = path.resolve(fromDir, importPath);
|
|
319
|
+
}
|
|
320
|
+
// Try different extensions
|
|
321
|
+
for (const ext of [
|
|
322
|
+
"",
|
|
323
|
+
".ts",
|
|
324
|
+
".tsx",
|
|
325
|
+
".js",
|
|
326
|
+
".jsx",
|
|
327
|
+
"/index.ts",
|
|
328
|
+
"/index.tsx",
|
|
329
|
+
"/index.js",
|
|
330
|
+
"/index.jsx",
|
|
331
|
+
]) {
|
|
332
|
+
const candidate = resolved + ext;
|
|
333
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
334
|
+
return candidate;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Trace from an entrypoint to find all reachable files with violations
|
|
341
|
+
*/
|
|
342
|
+
traceFromEntrypoint(entrypoint, projectPath) {
|
|
343
|
+
const violations = [];
|
|
344
|
+
const visited = new Set();
|
|
345
|
+
const queue = [{ file: entrypoint, chain: [entrypoint] }];
|
|
346
|
+
while (queue.length > 0) {
|
|
347
|
+
const { file, chain } = queue.shift();
|
|
348
|
+
if (visited.has(file)) continue;
|
|
349
|
+
visited.add(file);
|
|
350
|
+
const content = this.fileContents.get(file);
|
|
351
|
+
if (!content) continue;
|
|
352
|
+
// Check for banned patterns in file content
|
|
353
|
+
for (const banned of this.config.bannedImports) {
|
|
354
|
+
if (this.isFileAllowed(file, banned.allowedIn, projectPath)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const regex = banned.isRegex
|
|
358
|
+
? new RegExp(banned.pattern, "g")
|
|
359
|
+
: new RegExp(this.escapeRegex(banned.pattern), "g");
|
|
360
|
+
if (regex.test(content)) {
|
|
361
|
+
violations.push({
|
|
362
|
+
entrypoint: path.relative(projectPath, entrypoint),
|
|
363
|
+
bannedImport: path.relative(projectPath, file),
|
|
364
|
+
importChain: chain.map((f) => path.relative(projectPath, f)),
|
|
365
|
+
pattern: banned.pattern,
|
|
366
|
+
message: banned.message,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Add imports to queue
|
|
371
|
+
const node = this.importGraph.get(file);
|
|
372
|
+
if (node) {
|
|
373
|
+
for (const imp of node.imports) {
|
|
374
|
+
const resolved = this.resolveImport(imp, file, projectPath);
|
|
375
|
+
if (resolved && !visited.has(resolved)) {
|
|
376
|
+
queue.push({ file: resolved, chain: [...chain, resolved] });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return violations;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if a file matches any allowed patterns
|
|
385
|
+
*/
|
|
386
|
+
isFileAllowed(file, allowedPatterns, projectPath) {
|
|
387
|
+
const relativePath = path.relative(projectPath, file);
|
|
388
|
+
for (const pattern of allowedPatterns) {
|
|
389
|
+
if (this.matchGlob(relativePath, pattern)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Simple glob matching
|
|
397
|
+
*/
|
|
398
|
+
matchGlob(filePath, pattern) {
|
|
399
|
+
// Convert glob to regex
|
|
400
|
+
const regexPattern = pattern
|
|
401
|
+
.replace(/\*\*/g, "{{DOUBLE_STAR}}")
|
|
402
|
+
.replace(/\*/g, "[^/]*")
|
|
403
|
+
.replace(/\{\{DOUBLE_STAR\}\}/g, ".*")
|
|
404
|
+
.replace(/\?/g, ".");
|
|
405
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
406
|
+
return regex.test(filePath.replace(/\\/g, "/"));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Escape special regex characters
|
|
410
|
+
*/
|
|
411
|
+
escapeRegex(str) {
|
|
412
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Generate a human-readable report
|
|
416
|
+
*/
|
|
417
|
+
generateReport(result) {
|
|
418
|
+
const lines = [];
|
|
419
|
+
lines.push(
|
|
420
|
+
"╔══════════════════════════════════════════════════════════════╗",
|
|
421
|
+
);
|
|
422
|
+
lines.push(
|
|
423
|
+
"║ 🛡️ MockProof Build Gate Report 🛡️ ║",
|
|
424
|
+
);
|
|
425
|
+
lines.push(
|
|
426
|
+
"╚══════════════════════════════════════════════════════════════╝",
|
|
427
|
+
);
|
|
428
|
+
lines.push("");
|
|
429
|
+
if (result.verdict === "pass") {
|
|
430
|
+
lines.push(
|
|
431
|
+
"✅ VERDICT: PASS - No banned imports reachable from production",
|
|
432
|
+
);
|
|
433
|
+
lines.push("");
|
|
434
|
+
lines.push(
|
|
435
|
+
` Scanned ${result.scannedFiles} files from ${result.entrypoints.length} entrypoints`,
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
lines.push(
|
|
439
|
+
"❌ VERDICT: FAIL - Banned imports detected in production code",
|
|
440
|
+
);
|
|
441
|
+
lines.push("");
|
|
442
|
+
lines.push(` Found ${result.summary.totalViolations} violations`);
|
|
443
|
+
lines.push(
|
|
444
|
+
` ${result.summary.uniqueBannedImports} unique banned patterns`,
|
|
445
|
+
);
|
|
446
|
+
lines.push(
|
|
447
|
+
` ${result.summary.affectedEntrypoints} affected entrypoints`,
|
|
448
|
+
);
|
|
449
|
+
lines.push("");
|
|
450
|
+
lines.push("─".repeat(64));
|
|
451
|
+
lines.push("");
|
|
452
|
+
// Group violations by entrypoint
|
|
453
|
+
const byEntrypoint = new Map();
|
|
454
|
+
for (const v of result.violations) {
|
|
455
|
+
if (!byEntrypoint.has(v.entrypoint)) {
|
|
456
|
+
byEntrypoint.set(v.entrypoint, []);
|
|
457
|
+
}
|
|
458
|
+
byEntrypoint.get(v.entrypoint).push(v);
|
|
459
|
+
}
|
|
460
|
+
byEntrypoint.forEach((violations, entrypoint) => {
|
|
461
|
+
lines.push(`📍 Entrypoint: ${entrypoint}`);
|
|
462
|
+
lines.push("");
|
|
463
|
+
for (const v of violations) {
|
|
464
|
+
lines.push(` ❌ ${v.pattern}`);
|
|
465
|
+
lines.push(` Message: ${v.message}`);
|
|
466
|
+
lines.push(` Found in: ${v.bannedImport}`);
|
|
467
|
+
lines.push(` Import chain:`);
|
|
468
|
+
for (let i = 0; i < v.importChain.length; i++) {
|
|
469
|
+
const prefix = i === 0 ? " 📦" : " ↓";
|
|
470
|
+
lines.push(`${prefix} ${v.importChain[i]}`);
|
|
471
|
+
}
|
|
472
|
+
lines.push("");
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
lines.push("─".repeat(64));
|
|
477
|
+
lines.push(`Generated: ${result.timestamp}`);
|
|
478
|
+
return lines.join("\n");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
exports.ImportGraphScanner = ImportGraphScanner;
|
|
482
|
+
exports.importGraphScanner = new ImportGraphScanner();
|