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.
Files changed (119) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/mock-implementation.d.ts +1 -0
  6. package/dist/mock-implementation.d.ts.map +1 -0
  7. package/dist/mock-implementation.js +2 -0
  8. package/dist/mock-implementation.js.map +1 -0
  9. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
  10. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
  11. package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
  12. package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
  13. package/dist/mockproof/import-graph-scanner.d.ts +93 -0
  14. package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
  15. package/dist/mockproof/import-graph-scanner.js +411 -0
  16. package/dist/mockproof/import-graph-scanner.js.map +1 -0
  17. package/dist/mockproof/index.d.ts +10 -0
  18. package/dist/mockproof/index.d.ts.map +1 -0
  19. package/dist/mockproof/index.js +10 -0
  20. package/dist/mockproof/index.js.map +1 -0
  21. package/dist/reality-mode/auth-enforcer.d.ts +13 -0
  22. package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
  23. package/dist/reality-mode/auth-enforcer.js +90 -0
  24. package/dist/reality-mode/auth-enforcer.js.map +1 -0
  25. package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
  26. package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
  27. package/dist/reality-mode/explorer/critical-flows.js +463 -0
  28. package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
  29. package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
  30. package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
  31. package/dist/reality-mode/explorer/flow-parser.js +250 -0
  32. package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
  33. package/dist/reality-mode/explorer/index.d.ts +11 -0
  34. package/dist/reality-mode/explorer/index.d.ts.map +1 -0
  35. package/dist/reality-mode/explorer/index.js +11 -0
  36. package/dist/reality-mode/explorer/index.js.map +1 -0
  37. package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
  38. package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
  39. package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
  40. package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
  41. package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
  42. package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
  43. package/dist/reality-mode/explorer/surface-discovery.js +357 -0
  44. package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
  45. package/dist/reality-mode/explorer/types.d.ts +275 -0
  46. package/dist/reality-mode/explorer/types.d.ts.map +1 -0
  47. package/dist/reality-mode/explorer/types.js +8 -0
  48. package/dist/reality-mode/explorer/types.js.map +1 -0
  49. package/dist/reality-mode/fake-success-detector.d.ts +10 -0
  50. package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
  51. package/dist/reality-mode/fake-success-detector.js +76 -0
  52. package/dist/reality-mode/fake-success-detector.js.map +1 -0
  53. package/dist/reality-mode/index.d.ts +14 -0
  54. package/dist/reality-mode/index.d.ts.map +1 -0
  55. package/dist/reality-mode/index.js +14 -0
  56. package/dist/reality-mode/index.js.map +1 -0
  57. package/dist/reality-mode/reality-scanner.d.ts +48 -0
  58. package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
  59. package/dist/reality-mode/reality-scanner.js +516 -0
  60. package/dist/reality-mode/reality-scanner.js.map +1 -0
  61. package/dist/reality-mode/report-generator.d.ts +11 -0
  62. package/dist/reality-mode/report-generator.d.ts.map +1 -0
  63. package/dist/reality-mode/report-generator.js +233 -0
  64. package/dist/reality-mode/report-generator.js.map +1 -0
  65. package/dist/reality-mode/traffic-classifier.d.ts +14 -0
  66. package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
  67. package/dist/reality-mode/traffic-classifier.js +131 -0
  68. package/dist/reality-mode/traffic-classifier.js.map +1 -0
  69. package/dist/reality-mode/types.d.ts +90 -0
  70. package/dist/reality-mode/types.d.ts.map +1 -0
  71. package/dist/reality-mode/types.js +2 -0
  72. package/dist/reality-mode/types.js.map +1 -0
  73. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
  74. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
  75. package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
  76. package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
  77. package/dist/ship-badge/index.d.ts +9 -0
  78. package/dist/ship-badge/index.d.ts.map +1 -0
  79. package/dist/ship-badge/index.js +9 -0
  80. package/dist/ship-badge/index.js.map +1 -0
  81. package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
  82. package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
  83. package/dist/ship-badge/ship-badge-generator.js +681 -0
  84. package/dist/ship-badge/ship-badge-generator.js.map +1 -0
  85. package/package.json +20 -0
  86. package/src/index.ts +7 -0
  87. package/src/mock-implementation.ts +0 -0
  88. package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
  89. package/src/mockproof/import-graph-scanner.d.ts +93 -0
  90. package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
  91. package/src/mockproof/import-graph-scanner.js +482 -0
  92. package/src/mockproof/import-graph-scanner.ts +540 -0
  93. package/src/mockproof/index.ts +18 -0
  94. package/src/reality-mode/auth-enforcer.ts +97 -0
  95. package/src/reality-mode/explorer/critical-flows.ts +504 -0
  96. package/src/reality-mode/explorer/flow-parser.ts +293 -0
  97. package/src/reality-mode/explorer/index.ts +22 -0
  98. package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
  99. package/src/reality-mode/explorer/surface-discovery.ts +498 -0
  100. package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
  101. package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
  102. package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
  103. package/src/reality-mode/explorer/templates/github-action.yml +132 -0
  104. package/src/reality-mode/explorer/types.ts +356 -0
  105. package/src/reality-mode/fake-success-detector.ts +89 -0
  106. package/src/reality-mode/index.ts +19 -0
  107. package/src/reality-mode/reality-scanner.d.ts +123 -0
  108. package/src/reality-mode/reality-scanner.d.ts.map +1 -0
  109. package/src/reality-mode/reality-scanner.js +526 -0
  110. package/src/reality-mode/reality-scanner.ts +576 -0
  111. package/src/reality-mode/report-generator.ts +253 -0
  112. package/src/reality-mode/traffic-classifier.ts +169 -0
  113. package/src/reality-mode/types.ts +95 -0
  114. package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
  115. package/src/ship-badge/index.ts +16 -0
  116. package/src/ship-badge/ship-badge-generator.d.ts +136 -0
  117. package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
  118. package/src/ship-badge/ship-badge-generator.js +779 -0
  119. package/src/ship-badge/ship-badge-generator.ts +873 -0
@@ -0,0 +1,540 @@
1
+ /**
2
+ * MockProof Build Gate - Import Graph Scanner
3
+ *
4
+ * Scans the import graph from production entrypoints to detect
5
+ * banned imports (MockProvider, useMock, mock-context, localhost, etc.)
6
+ * that would ship to production.
7
+ *
8
+ * This is the "one rule, one red line" feature that vibecoders love.
9
+ */
10
+
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+
14
+ export interface BannedImport {
15
+ pattern: string;
16
+ message: string;
17
+ isRegex: boolean;
18
+ allowedIn: string[];
19
+ }
20
+
21
+ export interface ImportNode {
22
+ file: string;
23
+ imports: string[];
24
+ importedBy: string[];
25
+ }
26
+
27
+ export interface ViolationPath {
28
+ entrypoint: string;
29
+ bannedImport: string;
30
+ importChain: string[];
31
+ pattern: string;
32
+ message: string;
33
+ }
34
+
35
+ export interface MockProofResult {
36
+ verdict: "pass" | "fail";
37
+ violations: ViolationPath[];
38
+ scannedFiles: number;
39
+ entrypoints: string[];
40
+ timestamp: string;
41
+ summary: {
42
+ totalViolations: number;
43
+ uniqueBannedImports: number;
44
+ affectedEntrypoints: number;
45
+ };
46
+ }
47
+
48
+ export interface MockProofConfig {
49
+ entrypoints: string[];
50
+ bannedImports: BannedImport[];
51
+ excludeDirs: string[];
52
+ includeExtensions: string[];
53
+ }
54
+
55
+ const DEFAULT_BANNED_IMPORTS: BannedImport[] = [
56
+ {
57
+ pattern: "MockProvider",
58
+ message: "MockProvider should not be reachable from production entrypoints",
59
+ isRegex: false,
60
+ allowedIn: [
61
+ "**/__tests__/**",
62
+ "**/test/**",
63
+ "**/stories/**",
64
+ "**/landing/**",
65
+ "**/*.test.*",
66
+ "**/*.spec.*",
67
+ ],
68
+ },
69
+ {
70
+ pattern: "useMock",
71
+ message: "useMock hook should not be reachable from production entrypoints",
72
+ isRegex: false,
73
+ allowedIn: ["**/__tests__/**", "**/test/**", "**/stories/**"],
74
+ },
75
+ {
76
+ pattern: "mock-context",
77
+ message: "mock-context imports are not allowed in production",
78
+ isRegex: false,
79
+ allowedIn: ["**/__tests__/**", "**/test/**"],
80
+ },
81
+ {
82
+ pattern: "localhost:\\d+",
83
+ message: "Hardcoded localhost URLs will break in production",
84
+ isRegex: true,
85
+ allowedIn: [
86
+ "**/*.test.*",
87
+ "**/*.spec.*",
88
+ "**/docs/**",
89
+ "**/.env.example",
90
+ "**/e2e/**",
91
+ ],
92
+ },
93
+ {
94
+ pattern: "jsonplaceholder\\.typicode\\.com",
95
+ message: "JSONPlaceholder is a mock API - not for production",
96
+ isRegex: true,
97
+ allowedIn: ["**/__tests__/**", "**/docs/**", "**/examples/**"],
98
+ },
99
+ {
100
+ pattern: "\\.ngrok\\.io",
101
+ message: "ngrok URLs are temporary and will break in production",
102
+ isRegex: true,
103
+ allowedIn: ["**/__tests__/**", "**/docs/**"],
104
+ },
105
+ {
106
+ pattern: "sk_test_|pk_test_",
107
+ message: "Test API keys should not be in production code",
108
+ isRegex: true,
109
+ allowedIn: ["**/__tests__/**", "**/docs/**", "**/*.example"],
110
+ },
111
+ {
112
+ pattern: "demo_|inv_demo|fake_",
113
+ message: "Demo/fake identifiers detected - not for production",
114
+ isRegex: true,
115
+ allowedIn: ["**/__tests__/**", "**/docs/**"],
116
+ },
117
+ {
118
+ pattern: "DEMO_MODE|MOCK_MODE|USE_MOCKS",
119
+ message: "Feature flags for mock mode detected",
120
+ isRegex: true,
121
+ allowedIn: ["**/__tests__/**", "**/.env.example"],
122
+ },
123
+ ];
124
+
125
+ const DEFAULT_CONFIG: MockProofConfig = {
126
+ entrypoints: [
127
+ "src/app/layout.tsx",
128
+ "src/app/page.tsx",
129
+ "src/pages/_app.tsx",
130
+ "src/pages/index.tsx",
131
+ "src/index.tsx",
132
+ "src/main.tsx",
133
+ "apps/web-ui/src/app/layout.tsx",
134
+ "apps/web-ui/src/app/page.tsx",
135
+ "apps/api/src/index.ts",
136
+ "apps/api/src/main.ts",
137
+ ],
138
+ bannedImports: DEFAULT_BANNED_IMPORTS,
139
+ excludeDirs: [
140
+ "node_modules",
141
+ ".git",
142
+ ".next",
143
+ "dist",
144
+ "build",
145
+ "coverage",
146
+ "__tests__",
147
+ "__mocks__",
148
+ "test",
149
+ "tests",
150
+ "e2e",
151
+ "stories",
152
+ ".storybook",
153
+ ],
154
+ includeExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
155
+ };
156
+
157
+ export class ImportGraphScanner {
158
+ private config: MockProofConfig;
159
+ private importGraph: Map<string, ImportNode> = new Map();
160
+ private fileContents: Map<string, string> = new Map();
161
+
162
+ constructor(config: Partial<MockProofConfig> = {}) {
163
+ this.config = { ...DEFAULT_CONFIG, ...config };
164
+ }
165
+
166
+ /**
167
+ * Scan a project for banned imports reachable from production entrypoints
168
+ */
169
+ async scan(projectPath: string): Promise<MockProofResult> {
170
+ this.importGraph.clear();
171
+ this.fileContents.clear();
172
+
173
+ // 1. Find all source files
174
+ const files = await this.findSourceFiles(projectPath);
175
+
176
+ // 2. Build import graph
177
+ for (const file of files) {
178
+ await this.parseFile(file, projectPath);
179
+ }
180
+
181
+ // 3. Find valid entrypoints
182
+ const validEntrypoints = this.config.entrypoints
183
+ .map((ep) => path.join(projectPath, ep))
184
+ .filter((ep) => fs.existsSync(ep));
185
+
186
+ // 4. Trace from entrypoints to find violations
187
+ const violations: ViolationPath[] = [];
188
+
189
+ for (const entrypoint of validEntrypoints) {
190
+ const entrypointViolations = this.traceFromEntrypoint(
191
+ entrypoint,
192
+ projectPath,
193
+ );
194
+ violations.push(...entrypointViolations);
195
+ }
196
+
197
+ // 5. Build result
198
+ const uniqueBanned = new Set(violations.map((v) => v.bannedImport));
199
+ const affectedEntrypoints = new Set(violations.map((v) => v.entrypoint));
200
+
201
+ return {
202
+ verdict: violations.length > 0 ? "fail" : "pass",
203
+ violations,
204
+ scannedFiles: this.importGraph.size,
205
+ entrypoints: validEntrypoints.map((ep) => path.relative(projectPath, ep)),
206
+ timestamp: new Date().toISOString(),
207
+ summary: {
208
+ totalViolations: violations.length,
209
+ uniqueBannedImports: uniqueBanned.size,
210
+ affectedEntrypoints: affectedEntrypoints.size,
211
+ },
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Find all source files in the project
217
+ */
218
+ private async findSourceFiles(projectPath: string): Promise<string[]> {
219
+ const files: string[] = [];
220
+
221
+ const walk = async (dir: string): Promise<void> => {
222
+ try {
223
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
224
+
225
+ for (const entry of entries) {
226
+ const fullPath = path.join(dir, entry.name);
227
+
228
+ if (entry.isDirectory()) {
229
+ // Skip excluded directories
230
+ if (
231
+ !this.config.excludeDirs.includes(entry.name) &&
232
+ !entry.name.startsWith(".")
233
+ ) {
234
+ await walk(fullPath);
235
+ }
236
+ } else if (entry.isFile()) {
237
+ const ext = path.extname(entry.name);
238
+ if (this.config.includeExtensions.includes(ext)) {
239
+ files.push(fullPath);
240
+ }
241
+ }
242
+ }
243
+ } catch (error) {
244
+ // Skip directories that can't be read
245
+ }
246
+ };
247
+
248
+ await walk(projectPath);
249
+ return files;
250
+ }
251
+
252
+ /**
253
+ * Parse a file and extract its imports
254
+ */
255
+ private async parseFile(
256
+ filePath: string,
257
+ projectPath: string,
258
+ ): Promise<void> {
259
+ try {
260
+ const content = await fs.promises.readFile(filePath, "utf-8");
261
+ this.fileContents.set(filePath, content);
262
+
263
+ const imports = this.extractImports(content, filePath, projectPath);
264
+
265
+ const node: ImportNode = {
266
+ file: filePath,
267
+ imports,
268
+ importedBy: [],
269
+ };
270
+
271
+ this.importGraph.set(filePath, node);
272
+
273
+ // Update importedBy for resolved imports
274
+ for (const imp of imports) {
275
+ const resolved = this.resolveImport(imp, filePath, projectPath);
276
+ if (resolved && this.importGraph.has(resolved)) {
277
+ this.importGraph.get(resolved)!.importedBy.push(filePath);
278
+ }
279
+ }
280
+ } catch (error) {
281
+ // Skip files that can't be read
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Extract import statements from file content
287
+ */
288
+ private extractImports(
289
+ content: string,
290
+ filePath: string,
291
+ projectPath: string,
292
+ ): string[] {
293
+ const imports: string[] = [];
294
+
295
+ // ES6 imports: import X from 'Y', import { X } from 'Y', import 'Y'
296
+ const es6ImportRegex =
297
+ /import\s+(?:(?:\{[^}]*\}|[\w*]+(?:\s+as\s+\w+)?|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
298
+ let match;
299
+ while ((match = es6ImportRegex.exec(content)) !== null) {
300
+ if (match[1]) imports.push(match[1]);
301
+ }
302
+
303
+ // Dynamic imports: import('Y')
304
+ const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
305
+ while ((match = dynamicImportRegex.exec(content)) !== null) {
306
+ if (match[1]) imports.push(match[1]);
307
+ }
308
+
309
+ // CommonJS requires: require('Y')
310
+ const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
311
+ while ((match = requireRegex.exec(content)) !== null) {
312
+ if (match[1]) imports.push(match[1]);
313
+ }
314
+
315
+ return imports;
316
+ }
317
+
318
+ /**
319
+ * Resolve an import path to an absolute file path
320
+ */
321
+ private resolveImport(
322
+ importPath: string,
323
+ fromFile: string,
324
+ projectPath: string,
325
+ ): string | null {
326
+ // Skip node_modules imports
327
+ if (
328
+ !importPath.startsWith(".") &&
329
+ !importPath.startsWith("/") &&
330
+ !importPath.startsWith("@/")
331
+ ) {
332
+ return null;
333
+ }
334
+
335
+ const fromDir = path.dirname(fromFile);
336
+ let resolved: string;
337
+
338
+ if (importPath.startsWith("@/")) {
339
+ // Alias resolution (common in Next.js/React projects)
340
+ resolved = path.join(projectPath, "src", importPath.slice(2));
341
+ } else {
342
+ resolved = path.resolve(fromDir, importPath);
343
+ }
344
+
345
+ // Try different extensions
346
+ for (const ext of [
347
+ "",
348
+ ".ts",
349
+ ".tsx",
350
+ ".js",
351
+ ".jsx",
352
+ "/index.ts",
353
+ "/index.tsx",
354
+ "/index.js",
355
+ "/index.jsx",
356
+ ]) {
357
+ const candidate = resolved + ext;
358
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
359
+ return candidate;
360
+ }
361
+ }
362
+
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * Trace from an entrypoint to find all reachable files with violations
368
+ */
369
+ private traceFromEntrypoint(
370
+ entrypoint: string,
371
+ projectPath: string,
372
+ ): ViolationPath[] {
373
+ const violations: ViolationPath[] = [];
374
+ const visited = new Set<string>();
375
+ const queue: Array<{ file: string; chain: string[] }> = [
376
+ { file: entrypoint, chain: [entrypoint] },
377
+ ];
378
+
379
+ while (queue.length > 0) {
380
+ const { file, chain } = queue.shift()!;
381
+
382
+ if (visited.has(file)) continue;
383
+ visited.add(file);
384
+
385
+ const content = this.fileContents.get(file);
386
+ if (!content) continue;
387
+
388
+ // Check for banned patterns in file content
389
+ for (const banned of this.config.bannedImports) {
390
+ if (this.isFileAllowed(file, banned.allowedIn, projectPath)) {
391
+ continue;
392
+ }
393
+
394
+ const regex = banned.isRegex
395
+ ? new RegExp(banned.pattern, "g")
396
+ : new RegExp(this.escapeRegex(banned.pattern), "g");
397
+
398
+ if (regex.test(content)) {
399
+ violations.push({
400
+ entrypoint: path.relative(projectPath, entrypoint),
401
+ bannedImport: path.relative(projectPath, file),
402
+ importChain: chain.map((f) => path.relative(projectPath, f)),
403
+ pattern: banned.pattern,
404
+ message: banned.message,
405
+ });
406
+ }
407
+ }
408
+
409
+ // Add imports to queue
410
+ const node = this.importGraph.get(file);
411
+ if (node) {
412
+ for (const imp of node.imports) {
413
+ const resolved = this.resolveImport(imp, file, projectPath);
414
+ if (resolved && !visited.has(resolved)) {
415
+ queue.push({ file: resolved, chain: [...chain, resolved] });
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ return violations;
422
+ }
423
+
424
+ /**
425
+ * Check if a file matches any allowed patterns
426
+ */
427
+ private isFileAllowed(
428
+ file: string,
429
+ allowedPatterns: string[],
430
+ projectPath: string,
431
+ ): boolean {
432
+ const relativePath = path.relative(projectPath, file);
433
+
434
+ for (const pattern of allowedPatterns) {
435
+ if (this.matchGlob(relativePath, pattern)) {
436
+ return true;
437
+ }
438
+ }
439
+
440
+ return false;
441
+ }
442
+
443
+ /**
444
+ * Simple glob matching
445
+ */
446
+ private matchGlob(filePath: string, pattern: string): boolean {
447
+ // Convert glob to regex
448
+ const regexPattern = pattern
449
+ .replace(/\*\*/g, "{{DOUBLE_STAR}}")
450
+ .replace(/\*/g, "[^/]*")
451
+ .replace(/\{\{DOUBLE_STAR\}\}/g, ".*")
452
+ .replace(/\?/g, ".");
453
+
454
+ const regex = new RegExp(`^${regexPattern}$`);
455
+ return regex.test(filePath.replace(/\\/g, "/"));
456
+ }
457
+
458
+ /**
459
+ * Escape special regex characters
460
+ */
461
+ private escapeRegex(str: string): string {
462
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
463
+ }
464
+
465
+ /**
466
+ * Generate a human-readable report
467
+ */
468
+ generateReport(result: MockProofResult): string {
469
+ const lines: string[] = [];
470
+
471
+ lines.push(
472
+ "╔══════════════════════════════════════════════════════════════╗",
473
+ );
474
+ lines.push(
475
+ "║ 🛡️ MockProof Build Gate Report 🛡️ ║",
476
+ );
477
+ lines.push(
478
+ "╚══════════════════════════════════════════════════════════════╝",
479
+ );
480
+ lines.push("");
481
+
482
+ if (result.verdict === "pass") {
483
+ lines.push(
484
+ "✅ VERDICT: PASS - No banned imports reachable from production",
485
+ );
486
+ lines.push("");
487
+ lines.push(
488
+ ` Scanned ${result.scannedFiles} files from ${result.entrypoints.length} entrypoints`,
489
+ );
490
+ } else {
491
+ lines.push(
492
+ "❌ VERDICT: FAIL - Banned imports detected in production code",
493
+ );
494
+ lines.push("");
495
+ lines.push(` Found ${result.summary.totalViolations} violations`);
496
+ lines.push(
497
+ ` ${result.summary.uniqueBannedImports} unique banned patterns`,
498
+ );
499
+ lines.push(
500
+ ` ${result.summary.affectedEntrypoints} affected entrypoints`,
501
+ );
502
+ lines.push("");
503
+ lines.push("─".repeat(64));
504
+ lines.push("");
505
+
506
+ // Group violations by entrypoint
507
+ const byEntrypoint = new Map<string, ViolationPath[]>();
508
+ for (const v of result.violations) {
509
+ if (!byEntrypoint.has(v.entrypoint)) {
510
+ byEntrypoint.set(v.entrypoint, []);
511
+ }
512
+ byEntrypoint.get(v.entrypoint)!.push(v);
513
+ }
514
+
515
+ byEntrypoint.forEach((violations, entrypoint) => {
516
+ lines.push(`📍 Entrypoint: ${entrypoint}`);
517
+ lines.push("");
518
+
519
+ for (const v of violations) {
520
+ lines.push(` ❌ ${v.pattern}`);
521
+ lines.push(` Message: ${v.message}`);
522
+ lines.push(` Found in: ${v.bannedImport}`);
523
+ lines.push(` Import chain:`);
524
+ for (let i = 0; i < v.importChain.length; i++) {
525
+ const prefix = i === 0 ? " 📦" : " ↓";
526
+ lines.push(`${prefix} ${v.importChain[i]}`);
527
+ }
528
+ lines.push("");
529
+ }
530
+ });
531
+ }
532
+
533
+ lines.push("─".repeat(64));
534
+ lines.push(`Generated: ${result.timestamp}`);
535
+
536
+ return lines.join("\n");
537
+ }
538
+ }
539
+
540
+ export const importGraphScanner = new ImportGraphScanner();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MockProof Build Gate
3
+ *
4
+ * "One rule, one red line."
5
+ *
6
+ * Blocks MockProvider, useMock, mock-context, localhost, and other
7
+ * banned imports from reaching production entrypoints.
8
+ */
9
+
10
+ export {
11
+ ImportGraphScanner,
12
+ importGraphScanner,
13
+ type BannedImport,
14
+ type ImportNode,
15
+ type ViolationPath,
16
+ type MockProofResult,
17
+ type MockProofConfig,
18
+ } from "./import-graph-scanner";
@@ -0,0 +1,97 @@
1
+ export interface AuthCheckConfig {
2
+ baseUrl: string;
3
+ outputDir: string;
4
+ adminRoutes?: string[];
5
+ sensitiveRoutes?: string[];
6
+ }
7
+
8
+ export class AuthEnforcer {
9
+ /**
10
+ * Generate a Playwright test snippet to verify RBAC/Auth at runtime
11
+ */
12
+ generateAuthCheckTest(config: AuthCheckConfig): string {
13
+ const adminRoutes = config.adminRoutes || [
14
+ "/admin",
15
+ "/dashboard/settings",
16
+ "/api/admin",
17
+ ];
18
+ const sensitiveRoutes = config.sensitiveRoutes || [
19
+ "/api/users",
20
+ "/api/billing",
21
+ "/settings/billing",
22
+ ];
23
+ // Escape backslashes for Windows paths in the generated string
24
+ const outputDir = config.outputDir.replace(/\\/g, "\\\\");
25
+
26
+ return `
27
+ test('🛡️ Auth Enforcer: Runtime RBAC Check', async ({ page, request }) => {
28
+ console.log(' 🔒 Checking unauthenticated access to protected routes...');
29
+
30
+ const adminRoutes = ${JSON.stringify(adminRoutes)};
31
+ const sensitiveRoutes = ${JSON.stringify(sensitiveRoutes)};
32
+ const violations: { route: string, status: number, type: string }[] = [];
33
+
34
+ // 1. Clear cookies/storage to ensure we are unauthenticated
35
+ await page.context().clearCookies();
36
+ await page.context().clearPermissions();
37
+
38
+ // 2. Try to access admin routes (Frontend)
39
+ for (const route of adminRoutes) {
40
+ const target = \`\${'${config.baseUrl}'}\${route}\`;
41
+ try {
42
+ const response = await page.goto(target);
43
+ const url = page.url();
44
+
45
+ // If we are still on the admin route and got 200, that's a violation (unless it redirected to login)
46
+ if (response && response.status() === 200 && !url.includes('login') && !url.includes('signin') && !url.includes('sign-in')) {
47
+ // Double check we are actually seeing content, not just a loaded React shell that redirects later
48
+ // Use a heuristic: check for "Login" text or form
49
+ const loginForm = await page.$('input[type="password"]');
50
+ if (!loginForm) {
51
+ violations.push({ route, status: 200, type: 'Auth Mirage (Frontend)' });
52
+ console.log(\` ❌ Auth Mirage: Public access to \${route} (Status: 200, No Login Form)\`);
53
+ }
54
+ }
55
+ } catch (e) {
56
+ // Navigation failed, maybe good?
57
+ }
58
+ }
59
+
60
+ // 3. Try to access sensitive API endpoints (Backend)
61
+ for (const route of sensitiveRoutes) {
62
+ if (!route.startsWith('/api')) continue; // Only test API directly
63
+
64
+ const target = \`\${'${config.baseUrl}'}\${route}\`;
65
+ try {
66
+ const response = await request.get(target);
67
+
68
+ if (response.status() === 200) {
69
+ // Check if it returns actual data or just an error wrapper
70
+ const body = await response.json().catch(() => null);
71
+ // Assume if we get a JSON body without explicit error fields, it's a leak
72
+ if (body && !body.error && !body.redirect && !body.code) {
73
+ violations.push({ route, status: 200, type: 'Auth Mirage (Backend)' });
74
+ console.log(\` ❌ Auth Mirage: Unauthenticated API access to \${route}\`);
75
+ }
76
+ }
77
+ } catch (e) {
78
+ // Request failed
79
+ }
80
+ }
81
+
82
+ // Save auth results
83
+ const authResultPath = path.join('${outputDir}', 'auth-result.json');
84
+ await fs.promises.writeFile(authResultPath, JSON.stringify({ violations }, null, 2));
85
+
86
+ // Assert no violations
87
+ if (violations.length > 0) {
88
+ // Don't throw here if we want other tests to run?
89
+ // Actually Playwright stops on failure. But we saved the result.
90
+ expect(violations.length, \`Auth Mirage detected! \${violations.length} protected routes are accessible without auth\`).toBe(0);
91
+ } else {
92
+ console.log(' ✅ Auth checks passed. Protected routes are secure.');
93
+ }
94
+ });
95
+ `;
96
+ }
97
+ }