@0dai-dev/cli 2.4.0 → 2.5.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/bin/0dai.js +127 -1
- package/package.json +1 -1
package/bin/0dai.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
|
|
10
|
-
const VERSION = "2.
|
|
10
|
+
const VERSION = "2.5.0";
|
|
11
11
|
const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
|
|
12
12
|
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
|
|
13
13
|
const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
|
|
@@ -259,6 +259,130 @@ async function cmdDetect(target) {
|
|
|
259
259
|
console.log(`clis: ${(result.available_clis || []).join(",")}`);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
function cmdAudit(target) {
|
|
263
|
+
const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
|
|
264
|
+
const RE = process.stdout.isTTY ? "\x1b[31m" : ""; // red
|
|
265
|
+
const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
|
|
266
|
+
|
|
267
|
+
// Secret patterns: [label, regex, severity]
|
|
268
|
+
const PATTERNS = [
|
|
269
|
+
["Anthropic API key", /sk-ant-api[0-9A-Za-z_-]{20,}/g, "critical"],
|
|
270
|
+
["OpenAI API key", /sk-[A-Za-z0-9]{20,}/g, "critical"],
|
|
271
|
+
["GitHub PAT (ghp)", /ghp_[A-Za-z0-9]{36}/g, "critical"],
|
|
272
|
+
["GitHub PAT (gho)", /gho_[A-Za-z0-9]{36}/g, "critical"],
|
|
273
|
+
["GitHub fine-grained",/github_pat_[A-Za-z0-9_]{59}/g, "critical"],
|
|
274
|
+
["AWS access key", /AKIA[0-9A-Z]{16}/g, "critical"],
|
|
275
|
+
["AWS secret key", /aws_secret_access_key\s*[=:]\s*\S{20,}/gi,"critical"],
|
|
276
|
+
["Google API key", /AIza[0-9A-Za-z_-]{35}/g, "high"],
|
|
277
|
+
["Bearer token", /Bearer\s+[A-Za-z0-9_-]{32,}/g, "high"],
|
|
278
|
+
["Private key block", /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY/g, "critical"],
|
|
279
|
+
["Generic secret var", /(?:secret|password|passwd|pwd)\s*[=:]\s*["']?[A-Za-z0-9+/=_-]{12,}["']?/gi, "medium"],
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
// Files to scan
|
|
283
|
+
const SCAN_FILES = [
|
|
284
|
+
"CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules",
|
|
285
|
+
".codex/config.md", ".codex/instructions.md",
|
|
286
|
+
"opencode.json", ".mcp.json",
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
// Walk a directory recursively, return all file paths
|
|
290
|
+
function walk(dir, maxDepth = 6, _depth = 0) {
|
|
291
|
+
if (_depth > maxDepth) return [];
|
|
292
|
+
let results = [];
|
|
293
|
+
try {
|
|
294
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
295
|
+
if (entry.name.startsWith(".git") || entry.name === "node_modules") continue;
|
|
296
|
+
const full = path.join(dir, entry.name);
|
|
297
|
+
if (entry.isDirectory()) results = results.concat(walk(full, maxDepth, _depth + 1));
|
|
298
|
+
else results.push(full);
|
|
299
|
+
}
|
|
300
|
+
} catch { /* skip unreadable */ }
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const findings = []; // {file, line, label, severity, excerpt}
|
|
305
|
+
|
|
306
|
+
function scanContent(filePath, content) {
|
|
307
|
+
const lines = content.split("\n");
|
|
308
|
+
for (const [label, regex, severity] of PATTERNS) {
|
|
309
|
+
regex.lastIndex = 0;
|
|
310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
311
|
+
let m;
|
|
312
|
+
regex.lastIndex = 0;
|
|
313
|
+
while ((m = regex.exec(lines[i])) !== null) {
|
|
314
|
+
const val = m[0];
|
|
315
|
+
// Redact: show first 6 + ... + last 4 chars
|
|
316
|
+
const excerpt = val.length > 14 ? val.slice(0, 6) + "..." + val.slice(-4) : val.slice(0, 4) + "...";
|
|
317
|
+
findings.push({ file: filePath, line: i + 1, label, severity, excerpt });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Collect files to scan
|
|
324
|
+
const toScan = new Set();
|
|
325
|
+
|
|
326
|
+
for (const rel of SCAN_FILES) {
|
|
327
|
+
const p = path.join(target, rel);
|
|
328
|
+
if (fs.existsSync(p)) toScan.add(p);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const aiDir = path.join(target, "ai");
|
|
332
|
+
if (fs.existsSync(aiDir)) {
|
|
333
|
+
for (const f of walk(aiDir)) {
|
|
334
|
+
if (/\.(md|json|yaml|yml|txt|toml)$/.test(f)) toScan.add(f);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Warn about .env files (don't scan content, just flag existence)
|
|
339
|
+
const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
|
|
340
|
+
const foundEnv = envFiles.filter(e => fs.existsSync(path.join(target, e)));
|
|
341
|
+
|
|
342
|
+
console.log(`\n ${T}0dai audit${R} — scanning for leaked secrets\n`);
|
|
343
|
+
console.log(` ${D}target: ${target}${R}`);
|
|
344
|
+
console.log(` ${D}files: ${toScan.size} scanned${R}\n`);
|
|
345
|
+
|
|
346
|
+
for (const filePath of toScan) {
|
|
347
|
+
try {
|
|
348
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
349
|
+
scanContent(filePath, content);
|
|
350
|
+
} catch { /* skip */ }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (foundEnv.length > 0) {
|
|
354
|
+
console.log(` ${W}WARN${R} .env files detected — ensure they are in .gitignore`);
|
|
355
|
+
for (const e of foundEnv) console.log(` ${D}${e}${R}`);
|
|
356
|
+
console.log();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (findings.length === 0) {
|
|
360
|
+
console.log(` ${G}✓ No secrets found${R} in scanned files\n`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const critical = findings.filter(f => f.severity === "critical");
|
|
365
|
+
const high = findings.filter(f => f.severity === "high");
|
|
366
|
+
const medium = findings.filter(f => f.severity === "medium");
|
|
367
|
+
|
|
368
|
+
const colorFor = (s) => s === "critical" ? RE : s === "high" ? W : D;
|
|
369
|
+
|
|
370
|
+
for (const f of findings) {
|
|
371
|
+
const c = colorFor(f.severity);
|
|
372
|
+
const rel = path.relative(target, f.file);
|
|
373
|
+
console.log(` ${c}${f.severity.toUpperCase().padEnd(8)}${R} ${rel}:${f.line}`);
|
|
374
|
+
console.log(` ${D}${f.label}: ${f.excerpt}${R}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log();
|
|
378
|
+
if (critical.length > 0) console.log(` ${RE}${critical.length} critical${R} · ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
|
|
379
|
+
else console.log(` ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
|
|
380
|
+
|
|
381
|
+
console.log(` ${D}Tip: add secrets to .gitignore or use env vars, not plaintext files${R}\n`);
|
|
382
|
+
|
|
383
|
+
if (critical.length > 0) process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
262
386
|
function cmdDoctor(target) {
|
|
263
387
|
const ai = path.join(target, "ai");
|
|
264
388
|
if (!fs.existsSync(ai)) { log("no ai/ layer. Run '0dai init' first."); return; }
|
|
@@ -824,6 +948,7 @@ async function main() {
|
|
|
824
948
|
checkVersion();
|
|
825
949
|
|
|
826
950
|
switch (cmd) {
|
|
951
|
+
case "audit": cmdAudit(target); break;
|
|
827
952
|
case "init": await cmdInit(target); break;
|
|
828
953
|
case "sync": await cmdSync(target); break;
|
|
829
954
|
case "detect": await cmdDetect(target); break;
|
|
@@ -867,6 +992,7 @@ async function main() {
|
|
|
867
992
|
case "help": case "--help": case "-h":
|
|
868
993
|
console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
|
|
869
994
|
console.log("Commands:");
|
|
995
|
+
console.log(" audit Scan ai/ and agent configs for leaked secrets");
|
|
870
996
|
console.log(" init Initialize ai/ layer (via API)");
|
|
871
997
|
console.log(" sync Update ai/ layer (via API)");
|
|
872
998
|
console.log(" detect Show detected stack");
|