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.
- package/dist/adapters/docker.d.ts +8 -0
- package/dist/adapters/docker.d.ts.map +1 -0
- package/dist/adapters/docker.js +60 -0
- package/dist/adapters/docker.js.map +1 -0
- package/dist/adapters/import.d.ts +12 -0
- package/dist/adapters/import.d.ts.map +1 -0
- package/dist/adapters/import.js +76 -0
- package/dist/adapters/import.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +40 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/python.d.ts +9 -0
- package/dist/adapters/python.d.ts.map +1 -0
- package/dist/adapters/python.js +73 -0
- package/dist/adapters/python.js.map +1 -0
- package/dist/adapters/registry.d.ts +6 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +86 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/spawn.d.ts +9 -0
- package/dist/adapters/spawn.d.ts.map +1 -0
- package/dist/adapters/spawn.js +63 -0
- package/dist/adapters/spawn.js.map +1 -0
- package/dist/adapters/types.d.ts +35 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.d.ts.map +1 -0
- package/dist/branding.js +21 -0
- package/dist/branding.js.map +1 -0
- package/dist/commands/baselines.d.ts +14 -0
- package/dist/commands/baselines.d.ts.map +1 -0
- package/dist/commands/baselines.js +269 -0
- package/dist/commands/baselines.js.map +1 -0
- package/dist/commands/guard.d.ts +38 -0
- package/dist/commands/guard.d.ts.map +1 -0
- package/dist/commands/guard.js +307 -0
- package/dist/commands/guard.js.map +1 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +356 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/onepassword-migration.d.ts +23 -0
- package/dist/commands/onepassword-migration.d.ts.map +1 -0
- package/dist/commands/onepassword-migration.js +179 -0
- package/dist/commands/onepassword-migration.js.map +1 -0
- package/dist/commands/protect.d.ts +34 -0
- package/dist/commands/protect.d.ts.map +1 -0
- package/dist/commands/protect.js +642 -0
- package/dist/commands/protect.js.map +1 -0
- package/dist/commands/runtime.d.ts +28 -0
- package/dist/commands/runtime.d.ts.map +1 -0
- package/dist/commands/runtime.js +309 -0
- package/dist/commands/runtime.js.map +1 -0
- package/dist/commands/self-register.d.ts +39 -0
- package/dist/commands/self-register.d.ts.map +1 -0
- package/dist/commands/self-register.js +528 -0
- package/dist/commands/self-register.js.map +1 -0
- package/dist/commands/verify.d.ts +25 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +300 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/contextual/advisor.d.ts +12 -0
- package/dist/contextual/advisor.d.ts.map +1 -0
- package/dist/contextual/advisor.js +94 -0
- package/dist/contextual/advisor.js.map +1 -0
- package/dist/contextual/index.d.ts +3 -0
- package/dist/contextual/index.d.ts.map +1 -0
- package/dist/contextual/index.js +7 -0
- package/dist/contextual/index.js.map +1 -0
- package/dist/guided/attack-walkthrough.d.ts +13 -0
- package/dist/guided/attack-walkthrough.d.ts.map +1 -0
- package/dist/guided/attack-walkthrough.js +113 -0
- package/dist/guided/attack-walkthrough.js.map +1 -0
- package/dist/guided/wizard.d.ts +2 -0
- package/dist/guided/wizard.d.ts.map +1 -0
- package/dist/guided/wizard.js +108 -0
- package/dist/guided/wizard.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/dist/natural/index.d.ts +4 -0
- package/dist/natural/index.d.ts.map +1 -0
- package/dist/natural/index.js +9 -0
- package/dist/natural/index.js.map +1 -0
- package/dist/natural/intent-map.d.ts +7 -0
- package/dist/natural/intent-map.d.ts.map +1 -0
- package/dist/natural/intent-map.js +145 -0
- package/dist/natural/intent-map.js.map +1 -0
- package/dist/natural/llm-fallback.d.ts +8 -0
- package/dist/natural/llm-fallback.d.ts.map +1 -0
- package/dist/natural/llm-fallback.js +143 -0
- package/dist/natural/llm-fallback.js.map +1 -0
- package/dist/report/interactive-html.d.ts +51 -0
- package/dist/report/interactive-html.d.ts.map +1 -0
- package/dist/report/interactive-html.js +508 -0
- package/dist/report/interactive-html.js.map +1 -0
- package/dist/router.d.ts +23 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +132 -0
- package/dist/router.js.map +1 -0
- package/dist/semantic/command-index.json +182 -0
- package/dist/semantic/index.d.ts +3 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +28 -0
- package/dist/semantic/index.js.map +1 -0
- package/dist/semantic/search.d.ts +17 -0
- package/dist/semantic/search.d.ts.map +1 -0
- package/dist/semantic/search.js +123 -0
- package/dist/semantic/search.js.map +1 -0
- package/dist/util/action-prompt.d.ts +29 -0
- package/dist/util/action-prompt.d.ts.map +1 -0
- package/dist/util/action-prompt.js +126 -0
- package/dist/util/action-prompt.js.map +1 -0
- package/dist/util/advisories.d.ts +43 -0
- package/dist/util/advisories.d.ts.map +1 -0
- package/dist/util/advisories.js +229 -0
- package/dist/util/advisories.js.map +1 -0
- package/dist/util/colors.d.ts +9 -0
- package/dist/util/colors.d.ts.map +1 -0
- package/dist/util/colors.js +18 -0
- package/dist/util/colors.js.map +1 -0
- package/dist/util/credential-patterns.d.ts +38 -0
- package/dist/util/credential-patterns.d.ts.map +1 -0
- package/dist/util/credential-patterns.js +203 -0
- package/dist/util/credential-patterns.js.map +1 -0
- package/dist/util/detect.d.ts +11 -0
- package/dist/util/detect.d.ts.map +1 -0
- package/dist/util/detect.js +49 -0
- package/dist/util/detect.js.map +1 -0
- package/dist/util/format.d.ts +6 -0
- package/dist/util/format.d.ts.map +1 -0
- package/dist/util/format.js +49 -0
- package/dist/util/format.js.map +1 -0
- package/dist/util/report-submission.d.ts +64 -0
- package/dist/util/report-submission.d.ts.map +1 -0
- package/dist/util/report-submission.js +109 -0
- package/dist/util/report-submission.js.map +1 -0
- package/dist/util/spinner.d.ts +10 -0
- package/dist/util/spinner.d.ts.map +1 -0
- package/dist/util/spinner.js +38 -0
- package/dist/util/spinner.js.map +1 -0
- package/dist/util/version.d.ts +5 -0
- package/dist/util/version.d.ts.map +1 -0
- package/dist/util/version.js +24 -0
- package/dist/util/version.js.map +1 -0
- 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
|