opena2a-cli 0.1.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 (150) hide show
  1. package/dist/adapters/docker.d.ts +8 -0
  2. package/dist/adapters/docker.d.ts.map +1 -0
  3. package/dist/adapters/docker.js +60 -0
  4. package/dist/adapters/docker.js.map +1 -0
  5. package/dist/adapters/import.d.ts +12 -0
  6. package/dist/adapters/import.d.ts.map +1 -0
  7. package/dist/adapters/import.js +76 -0
  8. package/dist/adapters/import.js.map +1 -0
  9. package/dist/adapters/index.d.ts +9 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +40 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/python.d.ts +9 -0
  14. package/dist/adapters/python.d.ts.map +1 -0
  15. package/dist/adapters/python.js +73 -0
  16. package/dist/adapters/python.js.map +1 -0
  17. package/dist/adapters/registry.d.ts +6 -0
  18. package/dist/adapters/registry.d.ts.map +1 -0
  19. package/dist/adapters/registry.js +86 -0
  20. package/dist/adapters/registry.js.map +1 -0
  21. package/dist/adapters/spawn.d.ts +9 -0
  22. package/dist/adapters/spawn.d.ts.map +1 -0
  23. package/dist/adapters/spawn.js +63 -0
  24. package/dist/adapters/spawn.js.map +1 -0
  25. package/dist/adapters/types.d.ts +35 -0
  26. package/dist/adapters/types.d.ts.map +1 -0
  27. package/dist/adapters/types.js +3 -0
  28. package/dist/adapters/types.js.map +1 -0
  29. package/dist/branding.d.ts +3 -0
  30. package/dist/branding.d.ts.map +1 -0
  31. package/dist/branding.js +21 -0
  32. package/dist/branding.js.map +1 -0
  33. package/dist/commands/baselines.d.ts +14 -0
  34. package/dist/commands/baselines.d.ts.map +1 -0
  35. package/dist/commands/baselines.js +269 -0
  36. package/dist/commands/baselines.js.map +1 -0
  37. package/dist/commands/guard.d.ts +38 -0
  38. package/dist/commands/guard.d.ts.map +1 -0
  39. package/dist/commands/guard.js +307 -0
  40. package/dist/commands/guard.js.map +1 -0
  41. package/dist/commands/init.d.ts +14 -0
  42. package/dist/commands/init.d.ts.map +1 -0
  43. package/dist/commands/init.js +356 -0
  44. package/dist/commands/init.js.map +1 -0
  45. package/dist/commands/onepassword-migration.d.ts +23 -0
  46. package/dist/commands/onepassword-migration.d.ts.map +1 -0
  47. package/dist/commands/onepassword-migration.js +179 -0
  48. package/dist/commands/onepassword-migration.js.map +1 -0
  49. package/dist/commands/protect.d.ts +34 -0
  50. package/dist/commands/protect.d.ts.map +1 -0
  51. package/dist/commands/protect.js +642 -0
  52. package/dist/commands/protect.js.map +1 -0
  53. package/dist/commands/runtime.d.ts +28 -0
  54. package/dist/commands/runtime.d.ts.map +1 -0
  55. package/dist/commands/runtime.js +309 -0
  56. package/dist/commands/runtime.js.map +1 -0
  57. package/dist/commands/self-register.d.ts +39 -0
  58. package/dist/commands/self-register.d.ts.map +1 -0
  59. package/dist/commands/self-register.js +528 -0
  60. package/dist/commands/self-register.js.map +1 -0
  61. package/dist/commands/verify.d.ts +25 -0
  62. package/dist/commands/verify.d.ts.map +1 -0
  63. package/dist/commands/verify.js +300 -0
  64. package/dist/commands/verify.js.map +1 -0
  65. package/dist/contextual/advisor.d.ts +12 -0
  66. package/dist/contextual/advisor.d.ts.map +1 -0
  67. package/dist/contextual/advisor.js +94 -0
  68. package/dist/contextual/advisor.js.map +1 -0
  69. package/dist/contextual/index.d.ts +3 -0
  70. package/dist/contextual/index.d.ts.map +1 -0
  71. package/dist/contextual/index.js +7 -0
  72. package/dist/contextual/index.js.map +1 -0
  73. package/dist/guided/attack-walkthrough.d.ts +13 -0
  74. package/dist/guided/attack-walkthrough.d.ts.map +1 -0
  75. package/dist/guided/attack-walkthrough.js +113 -0
  76. package/dist/guided/attack-walkthrough.js.map +1 -0
  77. package/dist/guided/wizard.d.ts +2 -0
  78. package/dist/guided/wizard.d.ts.map +1 -0
  79. package/dist/guided/wizard.js +108 -0
  80. package/dist/guided/wizard.js.map +1 -0
  81. package/dist/index.d.ts +3 -0
  82. package/dist/index.d.ts.map +1 -0
  83. package/dist/index.js +326 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/natural/index.d.ts +4 -0
  86. package/dist/natural/index.d.ts.map +1 -0
  87. package/dist/natural/index.js +9 -0
  88. package/dist/natural/index.js.map +1 -0
  89. package/dist/natural/intent-map.d.ts +7 -0
  90. package/dist/natural/intent-map.d.ts.map +1 -0
  91. package/dist/natural/intent-map.js +145 -0
  92. package/dist/natural/intent-map.js.map +1 -0
  93. package/dist/natural/llm-fallback.d.ts +8 -0
  94. package/dist/natural/llm-fallback.d.ts.map +1 -0
  95. package/dist/natural/llm-fallback.js +143 -0
  96. package/dist/natural/llm-fallback.js.map +1 -0
  97. package/dist/report/interactive-html.d.ts +51 -0
  98. package/dist/report/interactive-html.d.ts.map +1 -0
  99. package/dist/report/interactive-html.js +508 -0
  100. package/dist/report/interactive-html.js.map +1 -0
  101. package/dist/router.d.ts +23 -0
  102. package/dist/router.d.ts.map +1 -0
  103. package/dist/router.js +132 -0
  104. package/dist/router.js.map +1 -0
  105. package/dist/semantic/command-index.json +182 -0
  106. package/dist/semantic/index.d.ts +3 -0
  107. package/dist/semantic/index.d.ts.map +1 -0
  108. package/dist/semantic/index.js +28 -0
  109. package/dist/semantic/index.js.map +1 -0
  110. package/dist/semantic/search.d.ts +17 -0
  111. package/dist/semantic/search.d.ts.map +1 -0
  112. package/dist/semantic/search.js +123 -0
  113. package/dist/semantic/search.js.map +1 -0
  114. package/dist/util/action-prompt.d.ts +29 -0
  115. package/dist/util/action-prompt.d.ts.map +1 -0
  116. package/dist/util/action-prompt.js +126 -0
  117. package/dist/util/action-prompt.js.map +1 -0
  118. package/dist/util/advisories.d.ts +43 -0
  119. package/dist/util/advisories.d.ts.map +1 -0
  120. package/dist/util/advisories.js +229 -0
  121. package/dist/util/advisories.js.map +1 -0
  122. package/dist/util/colors.d.ts +9 -0
  123. package/dist/util/colors.d.ts.map +1 -0
  124. package/dist/util/colors.js +18 -0
  125. package/dist/util/colors.js.map +1 -0
  126. package/dist/util/credential-patterns.d.ts +38 -0
  127. package/dist/util/credential-patterns.d.ts.map +1 -0
  128. package/dist/util/credential-patterns.js +203 -0
  129. package/dist/util/credential-patterns.js.map +1 -0
  130. package/dist/util/detect.d.ts +11 -0
  131. package/dist/util/detect.d.ts.map +1 -0
  132. package/dist/util/detect.js +49 -0
  133. package/dist/util/detect.js.map +1 -0
  134. package/dist/util/format.d.ts +6 -0
  135. package/dist/util/format.d.ts.map +1 -0
  136. package/dist/util/format.js +49 -0
  137. package/dist/util/format.js.map +1 -0
  138. package/dist/util/report-submission.d.ts +64 -0
  139. package/dist/util/report-submission.d.ts.map +1 -0
  140. package/dist/util/report-submission.js +109 -0
  141. package/dist/util/report-submission.js.map +1 -0
  142. package/dist/util/spinner.d.ts +10 -0
  143. package/dist/util/spinner.d.ts.map +1 -0
  144. package/dist/util/spinner.js +38 -0
  145. package/dist/util/spinner.js.map +1 -0
  146. package/dist/util/version.d.ts +5 -0
  147. package/dist/util/version.d.ts.map +1 -0
  148. package/dist/util/version.js +24 -0
  149. package/dist/util/version.js.map +1 -0
  150. package/package.json +47 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protect.d.ts","sourceRoot":"","sources":["../../src/commands/protect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0CH,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,gCAAgC;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsKtE"}
@@ -0,0 +1,642 @@
1
+ "use strict";
2
+ /**
3
+ * opena2a protect — Detect credentials and migrate to Secretless vault.
4
+ *
5
+ * Flow:
6
+ * 1. Run HMA CRED + DRIFT checks on the target directory
7
+ * 2. For each detected credential with a raw value:
8
+ * a. Store in Secretless SecretStore
9
+ * b. Replace in source file with environment variable reference
10
+ * c. Register broker policy (default: deny-all, must be explicitly allowed)
11
+ * d. Add to .env.example
12
+ * 3. Re-run scan to verify clean
13
+ * 4. Output migration report
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.protect = protect;
50
+ const fs = __importStar(require("node:fs"));
51
+ const path = __importStar(require("node:path"));
52
+ const colors_js_1 = require("../util/colors.js");
53
+ const spinner_js_1 = require("../util/spinner.js");
54
+ const format_js_1 = require("../util/format.js");
55
+ // --- Credential patterns (shared module) ---
56
+ const credential_patterns_js_1 = require("../util/credential-patterns.js");
57
+ // --- Core logic ---
58
+ /**
59
+ * Main protect command. Scans for credentials, migrates to vault, verifies clean.
60
+ */
61
+ async function protect(options) {
62
+ const startTime = Date.now();
63
+ const targetDir = path.resolve(options.targetDir);
64
+ if (!fs.existsSync(targetDir)) {
65
+ process.stderr.write((0, colors_js_1.red)(`Target directory not found: ${targetDir}\n`));
66
+ return 1;
67
+ }
68
+ if (options.dryRun) {
69
+ process.stdout.write((0, colors_js_1.yellow)('[DRY RUN] No files will be modified.\n\n'));
70
+ }
71
+ // Phase 1: Scan for credentials
72
+ const spinner = new spinner_js_1.Spinner('Scanning for credentials...');
73
+ spinner.start();
74
+ const matches = scanForCredentials(targetDir);
75
+ spinner.stop();
76
+ const isJson = options.format === 'json';
77
+ if (matches.length === 0) {
78
+ if (isJson) {
79
+ const report = {
80
+ targetDir,
81
+ totalFound: 0,
82
+ migrated: 0,
83
+ failed: 0,
84
+ skipped: 0,
85
+ results: [],
86
+ verificationPassed: true,
87
+ durationMs: Date.now() - startTime,
88
+ };
89
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
90
+ }
91
+ else {
92
+ process.stdout.write((0, colors_js_1.green)('No hardcoded credentials detected.\n'));
93
+ }
94
+ return 0;
95
+ }
96
+ if (!isJson) {
97
+ process.stdout.write((0, colors_js_1.bold)(`Found ${matches.length} credential(s) in ${targetDir}\n\n`));
98
+ // Show findings table
99
+ const findingsRows = matches.map(m => [
100
+ (0, format_js_1.severityLabel)(m.severity),
101
+ m.findingId,
102
+ m.title,
103
+ path.relative(targetDir, m.filePath) + ':' + m.line,
104
+ m.envVar,
105
+ ]);
106
+ process.stdout.write((0, format_js_1.table)(findingsRows, ['Severity', 'ID', 'Type', 'Location', 'Env Var']) + '\n\n');
107
+ // Show detailed explanations (non-CI only)
108
+ if (!options.ci) {
109
+ for (const m of matches) {
110
+ process.stdout.write((0, colors_js_1.bold)(`${m.findingId}: ${m.title}`) + '\n');
111
+ if (m.explanation) {
112
+ process.stdout.write((0, colors_js_1.dim)(' Why: ') + m.explanation + '\n');
113
+ }
114
+ if (m.businessImpact) {
115
+ process.stdout.write((0, colors_js_1.dim)(' Impact: ') + (0, colors_js_1.yellow)(m.businessImpact) + '\n');
116
+ }
117
+ process.stdout.write('\n');
118
+ }
119
+ }
120
+ }
121
+ if (options.dryRun) {
122
+ if (!isJson) {
123
+ process.stdout.write((0, colors_js_1.yellow)('[DRY RUN] Would migrate the above credentials.\n'));
124
+ process.stdout.write((0, colors_js_1.dim)('Run without --dry-run to apply changes.\n'));
125
+ }
126
+ // Generate HTML report even in dry-run mode
127
+ if (options.report) {
128
+ await writeHtmlReport(options.report, targetDir, matches, isJson);
129
+ }
130
+ return 0;
131
+ }
132
+ // Phase 2: Migrate credentials
133
+ if (!isJson) {
134
+ spinner.update('Migrating credentials to Secretless vault...');
135
+ spinner.start();
136
+ }
137
+ const results = await migrateCredentials(matches, targetDir, options);
138
+ if (!isJson)
139
+ spinner.stop();
140
+ const migrated = results.filter(r => r.stored && r.replaced).length;
141
+ const failed = results.filter(r => r.error).length;
142
+ const skipped = results.filter(r => !r.stored && !r.replaced && !r.error).length;
143
+ // Phase 3: Update .env.example
144
+ updateEnvExample(targetDir, results.filter(r => r.stored), isJson);
145
+ // Phase 4: Verification re-scan
146
+ let verificationPassed = true;
147
+ if (!options.skipVerify && migrated > 0) {
148
+ if (!isJson) {
149
+ spinner.update('Verifying migration...');
150
+ spinner.start();
151
+ }
152
+ const remainingMatches = scanForCredentials(targetDir)
153
+ .filter(m => {
154
+ // Exclude .env files from verification — credentials are supposed to be there
155
+ const basename = path.basename(m.filePath);
156
+ return !basename.startsWith('.env');
157
+ });
158
+ verificationPassed = remainingMatches.length === 0;
159
+ if (!isJson) {
160
+ spinner.stop();
161
+ if (verificationPassed) {
162
+ process.stdout.write((0, colors_js_1.green)('Verification passed: no credentials remain in source.\n\n'));
163
+ }
164
+ else {
165
+ process.stdout.write((0, colors_js_1.yellow)(`Verification: ${remainingMatches.length} credential(s) still detected.\n` +
166
+ 'Some credentials may require manual migration.\n\n'));
167
+ }
168
+ }
169
+ }
170
+ // Phase 5: Report
171
+ const durationMs = Date.now() - startTime;
172
+ const report = {
173
+ targetDir,
174
+ totalFound: matches.length,
175
+ migrated,
176
+ failed,
177
+ skipped,
178
+ results,
179
+ verificationPassed,
180
+ durationMs,
181
+ };
182
+ if (options.format === 'json') {
183
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
184
+ }
185
+ else {
186
+ printReport(report);
187
+ // Offer 1Password migration after successful credential migration
188
+ if (report.migrated > 0 && !options.ci) {
189
+ try {
190
+ const { offer1PasswordMigration } = await import('./onepassword-migration.js');
191
+ await offer1PasswordMigration({
192
+ credentialCount: report.migrated,
193
+ ci: options.ci,
194
+ });
195
+ }
196
+ catch {
197
+ // 1Password migration module not critical -- skip silently
198
+ }
199
+ }
200
+ }
201
+ // Generate interactive HTML report if --report path provided
202
+ if (options.report) {
203
+ await writeHtmlReport(options.report, targetDir, matches, isJson);
204
+ }
205
+ return failed > 0 ? 1 : 0;
206
+ }
207
+ // --- Scanning ---
208
+ function scanForCredentials(targetDir) {
209
+ const matches = [];
210
+ const seen = new Set(); // dedup by value+file
211
+ (0, credential_patterns_js_1.walkFiles)(targetDir, (filePath) => {
212
+ let content;
213
+ try {
214
+ content = fs.readFileSync(filePath, 'utf-8');
215
+ }
216
+ catch {
217
+ return; // skip unreadable files
218
+ }
219
+ const lines = content.split('\n');
220
+ for (const pattern of credential_patterns_js_1.CREDENTIAL_PATTERNS) {
221
+ // Reset regex lastIndex for global patterns
222
+ pattern.pattern.lastIndex = 0;
223
+ for (let i = 0; i < lines.length; i++) {
224
+ const line = lines[i];
225
+ let match;
226
+ // Clone regex to avoid shared state issues
227
+ const re = new RegExp(pattern.pattern.source, pattern.pattern.flags);
228
+ while ((match = re.exec(line)) !== null) {
229
+ // For capture group patterns, use group 1; otherwise full match
230
+ const value = match[1] ?? match[0];
231
+ const dedupKey = `${value}:${filePath}`;
232
+ if (seen.has(dedupKey))
233
+ continue;
234
+ seen.add(dedupKey);
235
+ // Skip if it looks like an env var reference already
236
+ if (isEnvVarReference(line, match.index))
237
+ continue;
238
+ const envVar = deriveEnvVarName(pattern, filePath, matches);
239
+ matches.push({
240
+ value,
241
+ filePath,
242
+ line: i + 1,
243
+ findingId: pattern.id,
244
+ envVar,
245
+ severity: pattern.severity,
246
+ title: pattern.title,
247
+ explanation: pattern.explanation,
248
+ businessImpact: pattern.businessImpact,
249
+ });
250
+ }
251
+ }
252
+ }
253
+ });
254
+ return matches;
255
+ }
256
+ function isEnvVarReference(line, matchIndex) {
257
+ // Check if the match is inside process.env.X, ${X}, $X, os.environ, etc.
258
+ const before = line.slice(0, matchIndex);
259
+ return /process\.env\.\w*$/.test(before) ||
260
+ /\$\{?\w*$/.test(before) ||
261
+ /os\.environ\[['"]?\w*$/.test(before) ||
262
+ /getenv\(['"]?\w*$/.test(before);
263
+ }
264
+ function deriveEnvVarName(pattern, _filePath, existingMatches) {
265
+ const base = pattern.envVarPrefix;
266
+ const existing = existingMatches.filter(m => m.envVar.startsWith(base));
267
+ if (existing.length === 0)
268
+ return base;
269
+ // If the same prefix already exists, append a number
270
+ return `${base}_${existing.length + 1}`;
271
+ }
272
+ // --- Migration ---
273
+ async function migrateCredentials(matches, targetDir, options) {
274
+ const results = [];
275
+ for (const credential of matches) {
276
+ try {
277
+ // Step 1: Store in Secretless vault
278
+ const stored = await storeInVault(credential);
279
+ // Step 2: Replace in source file
280
+ const replaced = replaceInSource(credential);
281
+ // Step 3: Create broker policy
282
+ const policyCreated = createBrokerPolicy(credential, targetDir);
283
+ results.push({
284
+ credential,
285
+ stored,
286
+ replaced,
287
+ policyCreated,
288
+ });
289
+ if (options.verbose) {
290
+ const status = stored && replaced ? (0, colors_js_1.green)('[OK]') : (0, colors_js_1.yellow)('[PARTIAL]');
291
+ process.stdout.write(`${status} ${credential.envVar} <- ${path.relative(targetDir, credential.filePath)}:${credential.line}\n`);
292
+ }
293
+ }
294
+ catch (err) {
295
+ results.push({
296
+ credential,
297
+ stored: false,
298
+ replaced: false,
299
+ policyCreated: false,
300
+ error: err instanceof Error ? err.message : String(err),
301
+ });
302
+ if (options.verbose) {
303
+ process.stderr.write((0, colors_js_1.red)(`[FAIL] ${credential.envVar}: ${err instanceof Error ? err.message : String(err)}\n`));
304
+ }
305
+ }
306
+ }
307
+ return results;
308
+ }
309
+ /**
310
+ * Store a credential value in the Secretless SecretStore.
311
+ * Uses dynamic import to avoid hard dependency on secretless-ai.
312
+ */
313
+ async function storeInVault(credential) {
314
+ try {
315
+ // Dynamic import -- secretless-ai may not be installed
316
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
317
+ const secretless = await Function('return import("secretless-ai")')();
318
+ const mod = 'default' in secretless ? secretless.default : secretless;
319
+ const { SecretStore } = mod;
320
+ const store = new SecretStore();
321
+ await store.setSecret(credential.envVar, credential.value);
322
+ return true;
323
+ }
324
+ catch {
325
+ // Secretless not available -- write to .env file as fallback
326
+ return storeInDotEnv(credential);
327
+ }
328
+ }
329
+ /**
330
+ * Fallback: append credential to .env file in the project root.
331
+ */
332
+ function storeInDotEnv(credential) {
333
+ const projectRoot = findProjectRoot(credential.filePath);
334
+ if (!projectRoot)
335
+ return false;
336
+ const envPath = path.join(projectRoot, '.env');
337
+ let content = '';
338
+ if (fs.existsSync(envPath)) {
339
+ content = fs.readFileSync(envPath, 'utf-8');
340
+ // Don't add if already present
341
+ if (content.includes(`${credential.envVar}=`))
342
+ return true;
343
+ if (!content.endsWith('\n'))
344
+ content += '\n';
345
+ }
346
+ content += `${credential.envVar}=${credential.value}\n`;
347
+ // Write with restricted permissions (0o600)
348
+ const fd = fs.openSync(envPath, 'w', 0o600);
349
+ fs.writeSync(fd, content);
350
+ fs.closeSync(fd);
351
+ return true;
352
+ }
353
+ /**
354
+ * Replace the hardcoded credential in the source file with an environment variable reference.
355
+ *
356
+ * For programming languages (JS, Python, Go, etc.), the credential is typically
357
+ * inside quotes: `apiKey: "sk-ant-..."`. We must strip those quotes so the result
358
+ * is `apiKey: process.env.ANTHROPIC_API_KEY` (code expression) rather than
359
+ * `apiKey: "process.env.ANTHROPIC_API_KEY"` (string literal, broken at runtime).
360
+ */
361
+ function replaceInSource(credential) {
362
+ const content = fs.readFileSync(credential.filePath, 'utf-8');
363
+ const ext = path.extname(credential.filePath).toLowerCase();
364
+ // Build the replacement string based on file type
365
+ const replacement = getEnvVarReplacement(credential.envVar, ext, content, credential.value);
366
+ if (!replacement)
367
+ return false;
368
+ let newContent;
369
+ if (shouldStripQuotes(ext)) {
370
+ // For programming languages, replace the entire quoted expression
371
+ // (including surrounding quotes) with the bare env var reference
372
+ const quotedDouble = `"${credential.value}"`;
373
+ const quotedSingle = `'${credential.value}'`;
374
+ if (content.includes(quotedDouble)) {
375
+ newContent = content.replace(quotedDouble, replacement);
376
+ }
377
+ else if (content.includes(quotedSingle)) {
378
+ newContent = content.replace(quotedSingle, replacement);
379
+ }
380
+ else {
381
+ // No quotes found (e.g., template literal or unquoted), replace value directly
382
+ newContent = content.replace(credential.value, replacement);
383
+ }
384
+ }
385
+ else {
386
+ // For config files (YAML, JSON, .env, etc.), replace value inside quotes
387
+ newContent = content.replace(credential.value, replacement);
388
+ }
389
+ if (newContent === content)
390
+ return false; // nothing changed
391
+ fs.writeFileSync(credential.filePath, newContent, 'utf-8');
392
+ return true;
393
+ }
394
+ /**
395
+ * Programming languages where env var references must NOT be inside string quotes.
396
+ */
397
+ function shouldStripQuotes(ext) {
398
+ return ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rb', '.java', '.kt', '.rs'].includes(ext);
399
+ }
400
+ /**
401
+ * Generate the appropriate env var reference for the file type.
402
+ */
403
+ function getEnvVarReplacement(envVar, ext, content, _original) {
404
+ // Detect language/framework from file extension and content
405
+ switch (ext) {
406
+ case '.ts':
407
+ case '.tsx':
408
+ case '.js':
409
+ case '.jsx':
410
+ case '.mjs':
411
+ case '.cjs':
412
+ return `process.env.${envVar}`;
413
+ case '.py':
414
+ return `os.environ.get('${envVar}')`;
415
+ case '.go':
416
+ return `os.Getenv("${envVar}")`;
417
+ case '.rb':
418
+ return `ENV['${envVar}']`;
419
+ case '.java':
420
+ case '.kt':
421
+ return `System.getenv("${envVar}")`;
422
+ case '.rs':
423
+ return `std::env::var("${envVar}").unwrap_or_default()`;
424
+ case '.yaml':
425
+ case '.yml':
426
+ return `\${${envVar}}`;
427
+ case '.toml':
428
+ case '.ini':
429
+ case '.cfg':
430
+ case '.conf':
431
+ return `\${${envVar}}`;
432
+ case '.env':
433
+ case '.sh':
434
+ case '.bash':
435
+ case '.zsh':
436
+ return `$${envVar}`;
437
+ case '.json':
438
+ // JSON doesn't support env var references natively.
439
+ // Replace with a placeholder that frameworks commonly understand.
440
+ return `\${${envVar}}`;
441
+ case '.dockerfile':
442
+ return `$${envVar}`;
443
+ default:
444
+ // For Dockerfiles without extension
445
+ if (content.includes('FROM ') || content.includes('RUN ')) {
446
+ return `$${envVar}`;
447
+ }
448
+ // Default to shell-style
449
+ return `\${${envVar}}`;
450
+ }
451
+ }
452
+ /**
453
+ * Create a deny-all broker policy for this credential.
454
+ * The user must explicitly add allow rules.
455
+ */
456
+ function createBrokerPolicy(credential, targetDir) {
457
+ const policyDir = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '.', '.secretless-ai');
458
+ try {
459
+ if (!fs.existsSync(policyDir)) {
460
+ fs.mkdirSync(policyDir, { recursive: true, mode: 0o700 });
461
+ }
462
+ const policyFile = path.join(policyDir, 'broker-policies.json');
463
+ let policies = [];
464
+ if (fs.existsSync(policyFile)) {
465
+ try {
466
+ const raw = fs.readFileSync(policyFile, 'utf-8');
467
+ const parsed = JSON.parse(raw);
468
+ policies = Array.isArray(parsed) ? parsed : parsed.rules ?? [];
469
+ }
470
+ catch {
471
+ // Corrupted file, start fresh
472
+ policies = [];
473
+ }
474
+ }
475
+ // Check if policy for this credential already exists
476
+ const existingPolicy = policies.find((p) => p.credentialSelector === credential.envVar);
477
+ if (existingPolicy)
478
+ return true;
479
+ // Add deny-all policy for this credential
480
+ const projectName = path.basename(targetDir);
481
+ policies.push({
482
+ id: `protect-${credential.envVar.toLowerCase()}-${Date.now()}`,
483
+ agentSelector: '*',
484
+ credentialSelector: credential.envVar,
485
+ constraints: {},
486
+ effect: 'deny',
487
+ comment: `Auto-generated by opena2a protect from ${projectName}. Add allow rules for authorized agents.`,
488
+ });
489
+ fs.writeFileSync(policyFile, JSON.stringify(policies, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
490
+ return true;
491
+ }
492
+ catch {
493
+ return false;
494
+ }
495
+ }
496
+ /**
497
+ * Update .env.example with the migrated variable names.
498
+ */
499
+ function updateEnvExample(targetDir, migratedResults, quiet = false) {
500
+ if (migratedResults.length === 0)
501
+ return;
502
+ const envExamplePath = path.join(targetDir, '.env.example');
503
+ let content = '';
504
+ if (fs.existsSync(envExamplePath)) {
505
+ content = fs.readFileSync(envExamplePath, 'utf-8');
506
+ if (!content.endsWith('\n'))
507
+ content += '\n';
508
+ }
509
+ let added = 0;
510
+ for (const result of migratedResults) {
511
+ const envVar = result.credential.envVar;
512
+ if (!content.includes(`${envVar}=`)) {
513
+ content += `${envVar}=\n`;
514
+ added++;
515
+ }
516
+ }
517
+ if (added > 0) {
518
+ fs.writeFileSync(envExamplePath, content, 'utf-8');
519
+ if (!quiet) {
520
+ process.stdout.write((0, colors_js_1.dim)(`Updated .env.example with ${added} variable(s).\n`));
521
+ }
522
+ }
523
+ }
524
+ // --- Reporting ---
525
+ function printReport(report) {
526
+ process.stdout.write('\n' + (0, colors_js_1.bold)('Migration Report') + '\n');
527
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
528
+ const rows = [];
529
+ for (const result of report.results) {
530
+ const status = result.error
531
+ ? (0, colors_js_1.red)('FAILED')
532
+ : result.stored && result.replaced
533
+ ? (0, colors_js_1.green)('MIGRATED')
534
+ : (0, colors_js_1.yellow)('PARTIAL');
535
+ rows.push([
536
+ status,
537
+ result.credential.findingId,
538
+ result.credential.envVar,
539
+ path.relative(report.targetDir, result.credential.filePath) + ':' + result.credential.line,
540
+ ]);
541
+ if (result.error) {
542
+ process.stdout.write((0, colors_js_1.dim)(` Error: ${result.error}\n`));
543
+ }
544
+ }
545
+ process.stdout.write((0, format_js_1.table)(rows, ['Status', 'Finding', 'Env Var', 'Location']) + '\n\n');
546
+ // Summary
547
+ process.stdout.write((0, colors_js_1.bold)('Summary: '));
548
+ const parts = [];
549
+ if (report.migrated > 0)
550
+ parts.push((0, colors_js_1.green)(`${report.migrated} migrated`));
551
+ if (report.skipped > 0)
552
+ parts.push((0, colors_js_1.yellow)(`${report.skipped} skipped`));
553
+ if (report.failed > 0)
554
+ parts.push((0, colors_js_1.red)(`${report.failed} failed`));
555
+ process.stdout.write(parts.join(', ') + '\n');
556
+ process.stdout.write((0, colors_js_1.dim)(`Completed in ${(0, format_js_1.formatDuration)(report.durationMs)}\n`));
557
+ if (report.migrated > 0) {
558
+ process.stdout.write('\n' + (0, colors_js_1.cyan)('Next steps:') + '\n');
559
+ process.stdout.write(' 1. Review changes: ' + (0, colors_js_1.dim)('git diff') + '\n');
560
+ process.stdout.write(' 2. Add .env to .gitignore if not already present\n');
561
+ process.stdout.write(' 3. Configure broker allow rules: ' + (0, colors_js_1.dim)('~/.secretless-ai/broker-policies.json') + '\n');
562
+ process.stdout.write(' 4. Re-scan to confirm: ' + (0, colors_js_1.dim)('opena2a scan .') + '\n');
563
+ process.stdout.write('\n' + (0, colors_js_1.dim)('Continue hardening:') + '\n');
564
+ process.stdout.write((0, colors_js_1.dim)(' opena2a guard sign Sign config files for tamper detection') + '\n');
565
+ process.stdout.write((0, colors_js_1.dim)(' opena2a runtime start Enable runtime monitoring') + '\n');
566
+ process.stdout.write((0, colors_js_1.dim)(' opena2a init Re-assess trust score after migration') + '\n');
567
+ }
568
+ }
569
+ // --- Utilities ---
570
+ async function writeHtmlReport(reportPath, targetDir, matches, quiet) {
571
+ try {
572
+ const { generateInteractiveHtml } = await import('../report/interactive-html.js');
573
+ const reportData = {
574
+ metadata: {
575
+ generatedAt: new Date().toISOString(),
576
+ toolVersion: '0.1.0',
577
+ targetName: path.basename(targetDir),
578
+ scanType: 'protect',
579
+ },
580
+ summary: {
581
+ totalFindings: matches.length,
582
+ bySeverity: countBySeverity(matches),
583
+ score: calculateScore(matches),
584
+ },
585
+ findings: matches.map(m => ({
586
+ id: m.findingId,
587
+ severity: m.severity,
588
+ title: m.title,
589
+ description: `Hardcoded ${m.title} found in source code.`,
590
+ explanation: m.explanation,
591
+ businessImpact: m.businessImpact,
592
+ category: m.findingId.startsWith('DRIFT') ? 'Scope Drift' : 'Credential Exposure',
593
+ file: path.relative(targetDir, m.filePath),
594
+ line: m.line,
595
+ fix: `Replace with environment variable: ${m.envVar}`,
596
+ passed: false,
597
+ })),
598
+ };
599
+ const html = generateInteractiveHtml(reportData);
600
+ fs.writeFileSync(reportPath, html, 'utf-8');
601
+ if (!quiet) {
602
+ process.stdout.write((0, colors_js_1.green)(`\nHTML report written to ${reportPath}\n`));
603
+ }
604
+ }
605
+ catch (err) {
606
+ process.stderr.write((0, colors_js_1.red)(`Failed to generate HTML report: ${err instanceof Error ? err.message : String(err)}\n`));
607
+ }
608
+ }
609
+ function countBySeverity(matches) {
610
+ const counts = {};
611
+ for (const m of matches) {
612
+ counts[m.severity] = (counts[m.severity] || 0) + 1;
613
+ }
614
+ return counts;
615
+ }
616
+ function calculateScore(matches) {
617
+ if (matches.length === 0)
618
+ return 100;
619
+ const weights = { critical: 25, high: 15, medium: 8, low: 3, info: 1 };
620
+ let penalty = 0;
621
+ for (const m of matches) {
622
+ penalty += weights[m.severity] || 5;
623
+ }
624
+ return Math.max(0, 100 - penalty);
625
+ }
626
+ function findProjectRoot(startPath) {
627
+ let dir = path.dirname(startPath);
628
+ const root = path.parse(dir).root;
629
+ while (dir !== root) {
630
+ if (fs.existsSync(path.join(dir, 'package.json')) ||
631
+ fs.existsSync(path.join(dir, 'go.mod')) ||
632
+ fs.existsSync(path.join(dir, 'Cargo.toml')) ||
633
+ fs.existsSync(path.join(dir, 'pyproject.toml')) ||
634
+ fs.existsSync(path.join(dir, 'setup.py')) ||
635
+ fs.existsSync(path.join(dir, '.git'))) {
636
+ return dir;
637
+ }
638
+ dir = path.dirname(dir);
639
+ }
640
+ return null;
641
+ }
642
+ //# sourceMappingURL=protect.js.map