@pretense/cli 0.6.2 → 0.6.5

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.
File without changes
File without changes
package/dist/index.js CHANGED
@@ -10385,7 +10385,6 @@ function listPresetIds() {
10385
10385
 
10386
10386
  // ../packages/scanner/dist/index.js
10387
10387
  import { gunzipSync } from "zlib";
10388
- import { createRequire } from "module";
10389
10388
  var SECRET_LABEL_TAILS = [
10390
10389
  // password family
10391
10390
  "password",
@@ -13279,259 +13278,6 @@ function resolveSafeSpan(original, map, viewStart, viewEnd, value, cost) {
13279
13278
  if (!spanContainsValue(slice, value, cost)) return null;
13280
13279
  return { start, end, value: slice };
13281
13280
  }
13282
- var _native;
13283
- function loadNative() {
13284
- if (_native !== void 0) return _native;
13285
- try {
13286
- const require2 = createRequire(import.meta.url);
13287
- const mod = require2("@pretense/scanner-rs");
13288
- if (typeof mod?.semanticScan !== "function") {
13289
- _native = null;
13290
- } else {
13291
- _native = mod;
13292
- }
13293
- } catch {
13294
- _native = null;
13295
- }
13296
- return _native;
13297
- }
13298
- var _astProbe;
13299
- function isAstCoreAvailable() {
13300
- if (_astProbe !== void 0) return _astProbe;
13301
- const native = loadNative();
13302
- if (!native) {
13303
- _astProbe = false;
13304
- return false;
13305
- }
13306
- try {
13307
- native.semanticScan("", "typescript");
13308
- _astProbe = true;
13309
- } catch {
13310
- _astProbe = false;
13311
- }
13312
- return _astProbe;
13313
- }
13314
- function makeByteToChar(text) {
13315
- let ascii = true;
13316
- for (let i = 0; i < text.length; i++) {
13317
- if (text.charCodeAt(i) > 127) {
13318
- ascii = false;
13319
- break;
13320
- }
13321
- }
13322
- if (ascii) return (b) => b;
13323
- const totalBytes = Buffer.byteLength(text, "utf8");
13324
- const table = new Int32Array(totalBytes + 1);
13325
- let byte = 0;
13326
- for (let charIdx = 0; charIdx < text.length; ) {
13327
- const cp = text.codePointAt(charIdx);
13328
- const unitLen = cp > 65535 ? 2 : 1;
13329
- const byteLen = Buffer.byteLength(String.fromCodePoint(cp), "utf8");
13330
- for (let k = 0; k < byteLen; k++) table[byte + k] = charIdx;
13331
- byte += byteLen;
13332
- charIdx += unitLen;
13333
- }
13334
- table[totalBytes] = text.length;
13335
- return (b) => {
13336
- if (b <= 0) return 0;
13337
- if (b >= totalBytes) return text.length;
13338
- return table[b];
13339
- };
13340
- }
13341
- function isInsideAnySpan(start, end, spans) {
13342
- for (const s of spans) {
13343
- if (s.start > start) break;
13344
- if (start >= s.start && end <= s.end) return true;
13345
- }
13346
- return false;
13347
- }
13348
- var CREDENTIAL_NAME = /(?:^|[._-]|(?<=[a-z0-9]))(?:api[_-]?key|api[_-]?secret|secret|secrets|token|password|passwd|pwd|access[_-]?key|access[_-]?token|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer[_-]?token|session[_-]?secret|encryption[_-]?key|signing[_-]?key)s?$/i;
13349
- var CREDENTIAL_SETTER = /^(?:set|with|configure|use|update|put|load|init)[_-]?(?:api[_-]?key|api[_-]?secret|secret|token|password|passwd|pwd|access[_-]?key|access[_-]?token|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer[_-]?token|credential|credentials|key)s?$/i;
13350
- function looksLikeCredentialName(name) {
13351
- return CREDENTIAL_NAME.test(name);
13352
- }
13353
- function looksLikeCredentialSetter(callee) {
13354
- const last = callee.split(".").pop() ?? callee;
13355
- return CREDENTIAL_SETTER.test(last);
13356
- }
13357
- function unquoteLiteral(literal) {
13358
- const t = literal.trim();
13359
- if (t.length < 2) return null;
13360
- const open = t[0];
13361
- const close = t[t.length - 1];
13362
- if (open === '"' && close === '"' || open === "'" && close === "'" || open === "`" && close === "`") {
13363
- return t.slice(1, -1);
13364
- }
13365
- const firstQuote = t.search(/["'`]/);
13366
- if (firstQuote >= 0) {
13367
- const q = t[firstQuote];
13368
- const lastQuote = t.lastIndexOf(q);
13369
- if (lastQuote > firstQuote) return t.slice(firstQuote + 1, lastQuote);
13370
- }
13371
- return null;
13372
- }
13373
- var STRING_LITERAL_RX = /^\s*["'`]/;
13374
- function dataFlowMatches(text, sem, nextId2) {
13375
- const matches = [];
13376
- const findings = [];
13377
- for (const edge of sem.edges) {
13378
- const isAssign = edge.kind === "Assignment" && looksLikeCredentialName(edge.from);
13379
- const isSetter = edge.kind === "Call" && looksLikeCredentialSetter(edge.from);
13380
- if (!isAssign && !isSetter) continue;
13381
- if (!STRING_LITERAL_RX.test(edge.to)) continue;
13382
- const inner = unquoteLiteral(edge.to);
13383
- if (inner === null || inner.length === 0) continue;
13384
- const span = locateLiteral(text, edge.to, edge.line);
13385
- const start = span && span.start >= 0 ? span.start : 0;
13386
- const end = span && span.end >= 0 ? span.end : 0;
13387
- const severity = "high";
13388
- const category = "secret";
13389
- const action = "redact";
13390
- const type = "dataflow-credential";
13391
- const id = nextId2();
13392
- const masked = maskCredential(inner);
13393
- matches.push({ id, category, type, value: inner, masked, start, end, severity, action, confidence: 0.85 });
13394
- findings.push({
13395
- ruleId: type,
13396
- value: inner,
13397
- masked,
13398
- severity,
13399
- category,
13400
- line: edge.line,
13401
- column: span?.column ?? 0,
13402
- start,
13403
- end,
13404
- action,
13405
- confidence: 0.85
13406
- });
13407
- }
13408
- return { matches, findings };
13409
- }
13410
- function maskCredential(value) {
13411
- if (value.length <= 8) return "***";
13412
- return value.slice(0, 4) + "..." + value.slice(-4);
13413
- }
13414
- function locateLiteral(text, literalSource, line) {
13415
- let lineStart = 0;
13416
- let curLine = 1;
13417
- for (let i = 0; i < text.length && curLine < line; i++) {
13418
- if (text.charCodeAt(i) === 10) {
13419
- curLine++;
13420
- lineStart = i + 1;
13421
- }
13422
- }
13423
- let idx = text.indexOf(literalSource, lineStart);
13424
- if (idx < 0) idx = text.indexOf(literalSource);
13425
- if (idx < 0) return null;
13426
- const lineNlBefore = text.lastIndexOf("\n", idx);
13427
- const column = lineNlBefore < 0 ? idx : idx - lineNlBefore - 1;
13428
- return { start: idx, end: idx + literalSource.length, column };
13429
- }
13430
- var SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
13431
- function rebuildFindings(matches, findingsByMatchId) {
13432
- const out = [];
13433
- for (const m of matches) {
13434
- const f = findingsByMatchId.get(m.id);
13435
- if (f) out.push(f);
13436
- }
13437
- return out;
13438
- }
13439
- function dedupeMatches(matches) {
13440
- const sorted = [...matches].sort(
13441
- (a, b) => a.start - b.start || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || b.confidence - a.confidence
13442
- );
13443
- const kept = [];
13444
- for (const m of sorted) {
13445
- const overlap = kept.find(
13446
- (k) => m.start < k.end && k.start < m.end && !(m.start === 0 && m.end === 0)
13447
- );
13448
- if (!overlap) {
13449
- kept.push(m);
13450
- }
13451
- }
13452
- const seenZero = /* @__PURE__ */ new Set();
13453
- return kept.filter((m) => {
13454
- if (m.start === 0 && m.end === 0) {
13455
- const key = `${m.type}:${m.value}`;
13456
- if (seenZero.has(key)) return false;
13457
- seenZero.add(key);
13458
- }
13459
- return true;
13460
- });
13461
- }
13462
- async function scanLevel2(text, language, opts = {}) {
13463
- const lang = language ?? opts.language ?? "typescript";
13464
- const base = opts.presidioUrl ? await scanWithPresidio(text, opts.presidioUrl, opts) : scan(text, opts);
13465
- const native = loadNative();
13466
- if (!native) return base;
13467
- let sem;
13468
- try {
13469
- sem = native.semanticScan(text, lang);
13470
- } catch {
13471
- return base;
13472
- }
13473
- const byteToChar = makeByteToChar(text);
13474
- const commentSpans = [];
13475
- for (const node of sem.nodes) {
13476
- if (node.class === "Comment") {
13477
- commentSpans.push({ start: byteToChar(node.start_byte), end: byteToChar(node.end_byte) });
13478
- }
13479
- }
13480
- commentSpans.sort((a, b) => a.start - b.start || a.end - b.end);
13481
- const findingsById = /* @__PURE__ */ new Map();
13482
- base.matches.forEach((m, i) => findingsById.set(m.id, base.findings[i] ?? toFinding(text, m)));
13483
- const survivingMatches = base.matches.filter(
13484
- (m) => m.start === m.end || !isInsideAnySpan(m.start, m.end, commentSpans)
13485
- );
13486
- let dfMatches = [];
13487
- if (opts.dataFlow !== false) {
13488
- let counter = 0;
13489
- const nextId2 = () => `l2df_${++counter}`;
13490
- const df = dataFlowMatches(text, sem, nextId2);
13491
- df.matches.forEach((m, i) => findingsById.set(m.id, df.findings[i] ?? toFinding(text, m)));
13492
- dfMatches = df.matches.filter((m) => !isInsideAnySpan(m.start, m.end, commentSpans));
13493
- }
13494
- const deduped = dedupeMatches([...survivingMatches, ...dfMatches]).sort(
13495
- (a, b) => a.start - b.start || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]
13496
- );
13497
- const dedupedFindings = rebuildFindings(deduped, findingsById).sort(
13498
- (a, b) => a.start - b.start || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]
13499
- );
13500
- const clean = deduped.filter((m) => m.action === "block" || m.action === "redact").length === 0;
13501
- return {
13502
- clean,
13503
- matches: deduped,
13504
- findings: dedupedFindings,
13505
- scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
13506
- durationMs: base.durationMs,
13507
- patternCount: base.patternCount,
13508
- // Bounded-work state is a property of the underlying scan and MUST survive
13509
- // this merge — dropping it here would re-create the silent-incomplete hole
13510
- // one layer up.
13511
- incomplete: base.incomplete,
13512
- limits: base.limits
13513
- };
13514
- }
13515
- function toFinding(text, m) {
13516
- const before = text.slice(0, m.start);
13517
- const line = (before.match(/\n/g) ?? []).length + 1;
13518
- const lastNl = before.lastIndexOf("\n");
13519
- const column = lastNl === -1 ? m.start : m.start - lastNl - 1;
13520
- return {
13521
- ruleId: m.type,
13522
- value: m.value,
13523
- masked: m.masked,
13524
- severity: m.severity,
13525
- category: m.category,
13526
- line,
13527
- column,
13528
- start: m.start,
13529
- end: m.end,
13530
- action: m.action,
13531
- confidence: m.confidence,
13532
- ...m.entropy !== void 0 ? { entropy: m.entropy } : {}
13533
- };
13534
- }
13535
13281
  var MIN_SPAN_VERIFY_CHARS = 1024 * 1024;
13536
13282
  var MAX_SPAN_VERIFY_CHARS = 256 * 1024 * 1024;
13537
13283
  var SPAN_VERIFY_CHARS_PER_CHAR = 64;
@@ -13878,60 +13624,6 @@ function scan(text, opts = {}) {
13878
13624
  limits
13879
13625
  };
13880
13626
  }
13881
- async function scanWithPresidio(text, presidioUrl, opts = {}) {
13882
- const { presidio: _presidio, ...syncOpts } = opts;
13883
- const base = scan(text, syncOpts);
13884
- const nlIndex = buildNewlineIndex(text);
13885
- try {
13886
- const resp = await fetch(`${presidioUrl}/analyze`, {
13887
- method: "POST",
13888
- headers: { "Content-Type": "application/json" },
13889
- body: JSON.stringify({ text, language: "en" }),
13890
- signal: AbortSignal.timeout(2e3)
13891
- });
13892
- if (!resp.ok) return base;
13893
- const presidioResults = await resp.json();
13894
- for (const pr of presidioResults) {
13895
- const alreadyCovered = base.matches.some((m) => m.start <= pr.start && m.end >= pr.end);
13896
- if (!alreadyCovered && pr.score >= 0.7) {
13897
- const value = text.slice(pr.start, pr.end);
13898
- const type = `presidio-${pr.entity_type.toLowerCase()}`;
13899
- const severity = pr.score >= 0.9 ? "high" : "medium";
13900
- base.matches.push({
13901
- id: nextId(),
13902
- category: "pii",
13903
- type,
13904
- value,
13905
- masked: "***",
13906
- start: pr.start,
13907
- end: pr.end,
13908
- severity,
13909
- action: "redact",
13910
- confidence: pr.score
13911
- });
13912
- const { line, column } = lineColFromOffset(text, pr.start, nlIndex);
13913
- base.findings.push({
13914
- ruleId: type,
13915
- value,
13916
- masked: "***",
13917
- severity,
13918
- category: "pii",
13919
- line,
13920
- column,
13921
- start: pr.start,
13922
- end: pr.end,
13923
- action: "redact",
13924
- confidence: pr.score
13925
- });
13926
- }
13927
- }
13928
- base.matches.sort((a, b) => a.start - b.start);
13929
- base.findings.sort((a, b) => a.start - b.start);
13930
- base.clean = base.matches.filter((m) => m.action === "block" || m.action === "redact").length === 0;
13931
- } catch {
13932
- }
13933
- return base;
13934
- }
13935
13627
 
13936
13628
  // ../packages/mutator/dist/index.js
13937
13629
  init_esm_shims();
@@ -15862,6 +15554,30 @@ function extractDeltas(line, provider) {
15862
15554
  delta[prop] = v;
15863
15555
  }
15864
15556
  });
15557
+ } else if (provider === "google") {
15558
+ const candidates = json["candidates"];
15559
+ if (!Array.isArray(candidates)) return null;
15560
+ for (let ci = 0; ci < candidates.length; ci++) {
15561
+ const cand = candidates[ci];
15562
+ const candIndex = typeof cand["index"] === "number" ? cand["index"] : ci;
15563
+ const content = cand["content"];
15564
+ if (!content) continue;
15565
+ const parts = content["parts"];
15566
+ if (!Array.isArray(parts)) continue;
15567
+ for (const part of parts) {
15568
+ if (typeof part["text"] !== "string") continue;
15569
+ const isThought = part["thought"] === true;
15570
+ refs.push({
15571
+ key: `g:${candIndex}:${isThought ? "thought" : "text"}`,
15572
+ index: candIndex,
15573
+ kind: isThought ? "google_thought" : "google_text",
15574
+ text: part["text"],
15575
+ set: (v) => {
15576
+ part["text"] = v;
15577
+ }
15578
+ });
15579
+ }
15580
+ }
15865
15581
  } else {
15866
15582
  const choices = json["choices"];
15867
15583
  if (!Array.isArray(choices) || choices.length === 0) return null;
@@ -15907,6 +15623,14 @@ function toJsonStringContextMap(map) {
15907
15623
  }
15908
15624
  return out;
15909
15625
  }
15626
+ function classifyStreamProvider(upstream) {
15627
+ const u = upstream.toLowerCase();
15628
+ if (u.includes("anthropic")) return "anthropic";
15629
+ if (u.includes("generativelanguage") || u.includes("googleapis") || u.includes("gemini")) {
15630
+ return "google";
15631
+ }
15632
+ return "openai";
15633
+ }
15910
15634
  async function streamWithRollingBuffer(upstreamResp, map, provider, requestId) {
15911
15635
  const buffers = /* @__PURE__ */ new Map();
15912
15636
  const bufferMeta = /* @__PURE__ */ new Map();
@@ -16021,6 +15745,12 @@ function buildFlushEvent(text, provider, kind, index) {
16021
15745
  const delta2 = kind === "input_json" ? { type: "input_json_delta", partial_json: text } : kind === "thinking" ? { type: "thinking_delta", thinking: text } : { type: "text_delta", text };
16022
15746
  return "data: " + JSON.stringify({ type: "content_block_delta", index, delta: delta2 });
16023
15747
  }
15748
+ if (provider === "google") {
15749
+ const part = kind === "google_thought" ? { text, thought: true } : { text };
15750
+ return "data: " + JSON.stringify({
15751
+ candidates: [{ content: { parts: [part], role: "model" }, index }]
15752
+ });
15753
+ }
16024
15754
  const delta = kind === "openai_tool_args" ? { tool_calls: [{ index, function: { arguments: text } }] } : { content: text };
16025
15755
  return "data: " + JSON.stringify({
16026
15756
  id: "pretense-flush",
@@ -16276,7 +16006,7 @@ function tokenizeBodyDeep(body, scanText) {
16276
16006
  };
16277
16007
  }
16278
16008
  async function handleStreaming(upstreamResp, map, requestId, upstream) {
16279
- const provider = upstream.includes("anthropic") ? "anthropic" : "openai";
16009
+ const provider = classifyStreamProvider(upstream);
16280
16010
  return streamWithRollingBuffer(upstreamResp, map, provider, requestId);
16281
16011
  }
16282
16012
  function createProxy(opts = {}) {
@@ -16750,7 +16480,10 @@ Upgrade to ${_upgradeTier === "enterprise" ? "Enterprise" : "Pro"} for ${_upgrad
16750
16480
  for (const [orig, syn] of globalMap.entries()) {
16751
16481
  if (!secretMap.has(syn)) reverseMap.set(orig, syn);
16752
16482
  }
16753
- if (body["stream"] === true) {
16483
+ const upstreamIsSse = (upstreamResp.headers.get("content-type") ?? "").includes(
16484
+ "text/event-stream"
16485
+ );
16486
+ if (body["stream"] === true || upstreamIsSse) {
16754
16487
  return handleStreaming(upstreamResp, reverseMap, requestId, upstream);
16755
16488
  }
16756
16489
  let respBody;
@@ -16987,71 +16720,13 @@ function filterExisting(files) {
16987
16720
  }
16988
16721
  });
16989
16722
  }
16990
- function langForFile(filePath) {
16991
- switch (filePath ? extname2(filePath).toLowerCase() : "") {
16992
- case ".py":
16993
- return "python";
16994
- case ".go":
16995
- return "go";
16996
- case ".java":
16997
- return "java";
16998
- case ".rb":
16999
- return "ruby";
17000
- case ".rs":
17001
- return "rust";
17002
- case ".cs":
17003
- return "csharp";
17004
- case ".js":
17005
- case ".jsx":
17006
- return "javascript";
17007
- case ".tsx":
17008
- case ".ts":
17009
- return "typescript";
17010
- default:
17011
- return "typescript";
17012
- }
17013
- }
17014
- function resolveScanLevel(opts) {
17015
- if (opts.ast) return 2;
17016
- const raw2 = opts.level;
17017
- if (raw2 === void 0 || raw2 === "1") return 1;
17018
- if (raw2 === "2") return 2;
17019
- console.error(chalk3.red(`
17020
- x Invalid --level: "${raw2}". Use 1 (regex) or 2 (AST/data-flow).
17021
- `));
17022
- process.exit(1);
17023
- }
17024
- var _astFallbackNoticePrinted = false;
17025
- function resolveLevelWithAst(opts) {
17026
- const level = resolveScanLevel(opts);
17027
- if (level === 1) return { level, astUsed: false };
17028
- const astUsed = isAstCoreAvailable();
17029
- if (!astUsed && !_astFallbackNoticePrinted) {
17030
- console.error(
17031
- chalk3.yellow("\n ! AST core (@pretense/scanner-rs) unavailable \u2014 falling back to L1 (regex).") + chalk3.dim("\n Build the native core to enable L2 data-flow detection.\n")
17032
- );
17033
- _astFallbackNoticePrinted = true;
17034
- }
17035
- return { level, astUsed };
17036
- }
17037
- function effectiveScanLevel(requested, astUsed) {
17038
- return requested === 2 && astUsed ? 2 : 1;
17039
- }
17040
- function levelLabel(level, astUsed) {
17041
- if (level === 1) return "L1 \xB7 regex";
17042
- return astUsed ? "L2 \xB7 AST + regex" : "L2 \u2192 L1 (AST unavailable)";
17043
- }
17044
16723
  async function scanContent(content, opts, filePath) {
17045
- const base = {
16724
+ return scan(content, {
17046
16725
  contextAware: true,
17047
16726
  entropyAnalysis: opts.entropy !== false,
17048
16727
  ...filePath ? { filePath } : {},
17049
16728
  ...opts.presetName ? { presetName: opts.presetName } : {}
17050
- };
17051
- if (resolveScanLevel(opts) === 2) {
17052
- return await scanLevel2(content, langForFile(filePath), base);
17053
- }
17054
- return scan(content, base);
16729
+ });
17055
16730
  }
17056
16731
  async function scanFile(filePath, opts) {
17057
16732
  const content = readFileSync5(filePath, "utf-8");
@@ -17142,7 +16817,6 @@ function resolveFormat(opts, defaultSarif = false) {
17142
16817
  return "text";
17143
16818
  }
17144
16819
  async function outputMultiFileResults(label, files, opts, { defaultSarif = false, ciMode = false } = {}) {
17145
- const { level, astUsed } = resolveLevelWithAst(opts);
17146
16820
  const sourceFiles = files.filter(isSupportedFile);
17147
16821
  if (sourceFiles.length === 0) {
17148
16822
  const fmt3 = resolveFormat(opts, defaultSarif);
@@ -17190,7 +16864,7 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
17190
16864
  console.log(chalk3.cyan(`
17191
16865
  Pretense Scan \u2014 ${label}
17192
16866
  `));
17193
- console.log(chalk3.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Level: ${levelLabel(level, astUsed)}
16867
+ console.log(chalk3.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()}
17194
16868
  `));
17195
16869
  for (const { file, result } of allResults) printFindings(file, result);
17196
16870
  console.log(chalk3.dim(`
@@ -17199,7 +16873,7 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
17199
16873
  process.exit(findingsExitCode);
17200
16874
  }
17201
16875
  function addScanOpts(cmd) {
17202
- return cmd.option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option("--level <n>", "Detection level: 1 = regex (default), 2 = AST/data-flow (falls back to 1 unless the native addon is built locally)", "1").option("--ast", "Alias for --level 2 (falls back to level 1 unless the native addon is built locally)").option(
16876
+ return cmd.option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option(
17203
16877
  "--policy <preset>",
17204
16878
  `Apply a compliance preset (${COMPLIANCE_PRESET_IDS.join(", ")}); overrides scan.compliancePreset in pretense.yaml`
17205
16879
  );
@@ -17293,7 +16967,7 @@ function pathSubcommand() {
17293
16967
  });
17294
16968
  }
17295
16969
  function scanCommand() {
17296
- const cmd = new Command3("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option("--level <n>", "Detection level: 1 = regex (default), 2 = AST/data-flow (falls back to 1 unless the native addon is built locally)", "1").option("--ast", "Alias for --level 2 (falls back to level 1 unless the native addon is built locally)").option(
16970
+ const cmd = new Command3("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option(
17297
16971
  "--policy <preset>",
17298
16972
  `Apply a compliance preset (${COMPLIANCE_PRESET_IDS.join(", ")}); overrides scan.compliancePreset in pretense.yaml`
17299
16973
  ).action(async (file, opts = {}) => {
@@ -17338,14 +17012,12 @@ function scanCommand() {
17338
17012
  for await (const chunk of process.stdin) chunks.push(chunk);
17339
17013
  content = Buffer.concat(chunks).toString("utf-8");
17340
17014
  }
17341
- const { level, astUsed } = resolveLevelWithAst(opts);
17342
- const effectiveLevel = effectiveScanLevel(level, astUsed);
17343
17015
  const rawResult = await scanContent(content, opts, file);
17344
17016
  const filteredMatches = filterBySeverity(rawResult.matches, opts?.severity);
17345
17017
  const result = { ...rawResult, matches: filteredMatches, clean: filteredMatches.length === 0 };
17346
17018
  const fmt2 = resolveFormat(opts ?? {});
17347
17019
  if (fmt2 === "json") {
17348
- console.log(JSON.stringify({ ...result, level: effectiveLevel, requestedLevel: level }, null, 2));
17020
+ console.log(JSON.stringify(result, null, 2));
17349
17021
  process.exit(result.clean ? 0 : 1);
17350
17022
  }
17351
17023
  if (fmt2 === "sarif") {
@@ -17361,7 +17033,7 @@ function scanCommand() {
17361
17033
  console.log(chalk3.cyan(`
17362
17034
  Pretense Scan \u2014 ${file ?? "stdin"}
17363
17035
  `));
17364
- console.log(chalk3.dim(` Patterns: ${getPatternCount()} | Level: ${levelLabel(level, astUsed)} | Duration: ${result.durationMs}ms
17036
+ console.log(chalk3.dim(` Patterns: ${getPatternCount()} | Duration: ${result.durationMs}ms
17365
17037
  `));
17366
17038
  if (result.clean) {
17367
17039
  console.log(chalk3.green(" \u2713 No secrets or PII detected\n"));
@@ -17567,8 +17239,12 @@ function logsCommand() {
17567
17239
  entries = store.getRecentEntries(limit);
17568
17240
  store.close();
17569
17241
  } catch {
17570
- console.log(chalk6.dim("\n No audit data available. The audit database has not been initialized yet."));
17571
- console.log(chalk6.dim(" Run " + chalk6.cyan("pretense start") + " and make some requests first.\n"));
17242
+ if (opts.json) {
17243
+ console.log("[]");
17244
+ return;
17245
+ }
17246
+ console.error(chalk6.dim("\n No audit data available. The audit database has not been initialized yet."));
17247
+ console.error(chalk6.dim(" Run " + chalk6.cyan("pretense start") + " and make some requests first.\n"));
17572
17248
  return;
17573
17249
  }
17574
17250
  if (opts.json) {
@@ -18791,7 +18467,7 @@ import { Command as Command14 } from "commander";
18791
18467
  import chalk13 from "chalk";
18792
18468
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, chmodSync as chmodSync6 } from "fs";
18793
18469
  import { createHash as createHash3 } from "crypto";
18794
- import { resolve as resolve4, extname as extname4 } from "path";
18470
+ import { resolve as resolve4 } from "path";
18795
18471
 
18796
18472
  // src/secret-crypto.ts
18797
18473
  init_esm_shims();
@@ -18887,106 +18563,43 @@ var REVERSAL_MAP_VERSION = 2;
18887
18563
  function sha256Hex2(s) {
18888
18564
  return createHash3("sha256").update(s, "utf8").digest("hex");
18889
18565
  }
18890
- function langForFile2(filePath) {
18891
- switch (extname4(filePath).toLowerCase()) {
18892
- case ".py":
18893
- return "python";
18894
- case ".go":
18895
- return "go";
18896
- case ".java":
18897
- return "java";
18898
- case ".rb":
18899
- return "ruby";
18900
- case ".rs":
18901
- return "rust";
18902
- case ".cs":
18903
- return "csharp";
18904
- case ".js":
18905
- case ".jsx":
18906
- return "javascript";
18907
- case ".tsx":
18908
- case ".ts":
18909
- return "typescript";
18910
- default:
18911
- return "typescript";
18912
- }
18913
- }
18914
- function resolveMutateLevel(opts) {
18915
- if (opts.ast) return 2;
18916
- const raw2 = opts.level;
18917
- if (raw2 === void 0 || raw2 === "1") return 1;
18918
- if (raw2 === "2") return 2;
18919
- console.error(chalk13.red(`
18920
- x Invalid --level: "${raw2}". Use 1 (regex) or 2 (AST/data-flow).
18921
- `));
18922
- process.exit(1);
18923
- }
18924
- function overlaps2(aOff, aLen, bOff, bLen) {
18925
- return aOff < bOff + bLen && bOff < aOff + aLen;
18926
- }
18927
- function innerValueFinding(content, m) {
18928
- if (m.end <= m.start || m.value.length === 0) return null;
18929
- const raw2 = content.slice(m.start, m.end);
18930
- const idx = raw2.indexOf(m.value);
18931
- if (idx < 0) return null;
18932
- return { value: m.value, offset: m.start + idx, length: m.value.length, type: m.type };
18933
- }
18934
- async function collectMutationFindings(content, file, level) {
18566
+ async function collectMutationFindings(content, file) {
18935
18567
  const l1 = scan(content, { contextAware: true, entropyAnalysis: true, filePath: file });
18936
- const findings = l1.matches.map((m) => ({
18568
+ return l1.matches.map((m) => ({
18937
18569
  value: m.value,
18938
18570
  offset: m.start,
18939
18571
  length: m.end - m.start,
18940
18572
  type: m.type
18941
18573
  }));
18942
- if (level === 2) {
18943
- if (!isAstCoreAvailable()) {
18944
- console.error(
18945
- chalk13.yellow(" ! AST core (@pretense/scanner-rs) unavailable \u2014 mutating with L1 (regex).")
18946
- );
18947
- }
18948
- const l2 = await scanLevel2(content, langForFile2(file), {
18949
- contextAware: true,
18950
- entropyAnalysis: true,
18951
- filePath: file
18952
- });
18953
- for (const m of l2.matches) {
18954
- if (m.type !== "dataflow-credential") continue;
18955
- const f = innerValueFinding(content, m);
18956
- if (!f) continue;
18957
- if (findings.some((e) => overlaps2(f.offset, f.length, e.offset, e.length))) continue;
18958
- findings.push(f);
18959
- }
18960
- }
18961
- return findings;
18962
18574
  }
18963
18575
  async function runMutate(file, opts = {}) {
18964
- const level = resolveMutateLevel(opts);
18965
- let content;
18576
+ let raw2;
18966
18577
  try {
18967
- content = readFileSync14(file, "utf-8");
18578
+ raw2 = readFileSync14(file);
18968
18579
  } catch {
18969
18580
  console.error(chalk13.red(`
18970
18581
  x Cannot read file: ${file}
18971
18582
  `));
18972
18583
  return 1;
18973
18584
  }
18974
- const findings = await collectMutationFindings(content, file, level);
18585
+ const content = raw2.toString("utf-8");
18586
+ if (!Buffer.from(content, "utf-8").equals(raw2)) {
18587
+ console.error(
18588
+ chalk13.red(`
18589
+ x Not valid UTF-8: ${file}`) + chalk13.dim(`
18590
+ Mutating would replace the undecodable bytes with U+FFFD and the`) + chalk13.dim(`
18591
+ original content could not be restored. File left untouched.
18592
+ `)
18593
+ );
18594
+ return 1;
18595
+ }
18596
+ const findings = await collectMutationFindings(content, file);
18975
18597
  if (findings.length === 0) {
18976
18598
  console.error(chalk13.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
18977
18599
  if (!opts.inPlace && !opts.output) process.stdout.write(content);
18978
18600
  return 0;
18979
18601
  }
18980
18602
  const result = mutateSecrets(content, findings);
18981
- if (opts.inPlace) {
18982
- writeFileSync9(file, result.content);
18983
- console.error(chalk13.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
18984
- } else if (opts.output) {
18985
- writeFileSync9(opts.output, result.content);
18986
- console.error(chalk13.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
18987
- } else {
18988
- process.stdout.write(result.content);
18989
- }
18990
18603
  if (opts.map) {
18991
18604
  const absFile = resolve4(file);
18992
18605
  const projectKey = getOrCreateProjectKey(findProjectRoot(absFile), opts.keysDir);
@@ -19001,7 +18614,18 @@ async function runMutate(file, opts = {}) {
19001
18614
  type: m.type
19002
18615
  }))
19003
18616
  };
19004
- writeFileSync9(opts.map, JSON.stringify(mapFile, null, 2), { mode: 384 });
18617
+ try {
18618
+ writeFileSync9(opts.map, JSON.stringify(mapFile, null, 2), { mode: 384 });
18619
+ } catch (e) {
18620
+ console.error(
18621
+ chalk13.red(`
18622
+ x Cannot write reversal map: ${opts.map}`) + chalk13.dim(`
18623
+ ${e.message}`) + chalk13.dim(`
18624
+ Nothing was mutated \u2014 without a map the original could not be restored.
18625
+ `)
18626
+ );
18627
+ return 1;
18628
+ }
19005
18629
  try {
19006
18630
  chmodSync6(opts.map, 384);
19007
18631
  } catch {
@@ -19010,13 +18634,32 @@ async function runMutate(file, opts = {}) {
19010
18634
  chalk13.green(` \u2713 Wrote encrypted reversal map \u2192 ${opts.map}`) + chalk13.dim(" (AES-256-GCM, mode 0600)")
19011
18635
  );
19012
18636
  }
18637
+ try {
18638
+ if (opts.inPlace) {
18639
+ writeFileSync9(file, result.content);
18640
+ console.error(chalk13.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
18641
+ } else if (opts.output) {
18642
+ writeFileSync9(opts.output, result.content);
18643
+ console.error(chalk13.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
18644
+ } else {
18645
+ process.stdout.write(result.content);
18646
+ }
18647
+ } catch (e) {
18648
+ console.error(
18649
+ chalk13.red(`
18650
+ x Cannot write mutated content: ${opts.output ?? file}`) + chalk13.dim(`
18651
+ ${e.message}
18652
+ `)
18653
+ );
18654
+ return 1;
18655
+ }
19013
18656
  if (opts.showMap) {
19014
18657
  console.error(JSON.stringify(result.mutations, null, 2));
19015
18658
  }
19016
18659
  return 0;
19017
18660
  }
19018
18661
  function mutateCommand() {
19019
- return new Command14("mutate").description("Mutate secrets in a file to deterministic synthetics").argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file").option("-o, --output <path>", "Write mutated content to a specific file").option("--map <path>", "Write the encrypted reversal map (for `pretense reverse`) to a file").option("--level <n>", "Detection level: 1 = regex (default), 2 = AST/data-flow", "1").option("--ast", "Alias for --level 2 (AST/data-flow analysis)").option("--show-map", "Print mutation map JSON to stderr").action(async (file, opts) => {
18662
+ return new Command14("mutate").description("Mutate secrets in a file to deterministic synthetics").argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file").option("-o, --output <path>", "Write mutated content to a specific file").option("--map <path>", "Write the encrypted reversal map (for `pretense reverse`) to a file").option("--show-map", "Print mutation map JSON to stderr").action(async (file, opts) => {
19020
18663
  const code = await runMutate(file, opts);
19021
18664
  process.exit(code);
19022
18665
  });
@@ -19527,7 +19170,7 @@ function findClosestCommand(input, knownCommands) {
19527
19170
  return bestDist <= 3 ? best : null;
19528
19171
  }
19529
19172
  var program = new Command20();
19530
- program.name("pretense").description(chalk19.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version("0.6.2", "-v, --version").addHelpText("after", `
19173
+ program.name("pretense").description(chalk19.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version("0.6.5", "-v, --version").addHelpText("after", `
19531
19174
  ${chalk19.bold("Quick start:")}
19532
19175
  ${chalk19.cyan("pretense scan")} ${chalk19.dim("<file|dir>")} Scan for secrets and PII
19533
19176
  ${chalk19.cyan("pretense mutate")} ${chalk19.dim("<file>")} Preview code mutations
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pretense/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.5",
4
4
  "description": "Local-first AI firewall that mutates proprietary code before sending to LLM APIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,14 +11,6 @@
11
11
  "README.md",
12
12
  "LICENSE"
13
13
  ],
14
- "scripts": {
15
- "build": "tsup",
16
- "dev": "tsx src/index.ts",
17
- "prepublishOnly": "pnpm build",
18
- "postinstall": "node -e \"fs.existsSync('dist/postinstall.js') && import('./dist/postinstall.js')\" || true",
19
- "test": "vitest run",
20
- "lint": "tsc --noEmit"
21
- },
22
14
  "dependencies": {
23
15
  "better-sqlite3": "^11.0.0",
24
16
  "chalk": "^5.3.0",
@@ -26,19 +18,19 @@
26
18
  "yaml": "^2.4.0"
27
19
  },
28
20
  "devDependencies": {
29
- "@pretense/billing": "workspace:*",
30
- "@pretense/compliance-engine": "workspace:*",
31
- "@pretense/protocol": "workspace:*",
32
- "@pretense/learner": "workspace:*",
33
- "@pretense/mutator": "workspace:*",
34
- "@pretense/proxy": "workspace:*",
35
- "@pretense/scanner": "workspace:*",
36
- "@pretense/store": "workspace:*",
37
21
  "@types/node": "^20.0.0",
38
22
  "tsup": "^8.0.0",
39
23
  "tsx": "^4.0.0",
40
24
  "typescript": "^5.4.0",
41
- "vitest": "^1.0.0"
25
+ "vitest": "^1.0.0",
26
+ "@pretense/protocol": "0.1.0",
27
+ "@pretense/learner": "0.2.0",
28
+ "@pretense/mutator": "0.2.0",
29
+ "@pretense/scanner": "0.2.0",
30
+ "@pretense/store": "0.2.0",
31
+ "@pretense/proxy": "0.1.0",
32
+ "@pretense/compliance-engine": "0.1.0",
33
+ "@pretense/billing": "0.1.0"
42
34
  },
43
35
  "publishConfig": {
44
36
  "access": "public",
@@ -78,5 +70,12 @@
78
70
  },
79
71
  "engines": {
80
72
  "node": ">=20.0.0"
73
+ },
74
+ "scripts": {
75
+ "build": "tsup",
76
+ "dev": "tsx src/index.ts",
77
+ "postinstall": "node -e \"fs.existsSync('dist/postinstall.js') && import('./dist/postinstall.js')\" || true",
78
+ "test": "vitest run",
79
+ "lint": "tsc --noEmit"
81
80
  }
82
- }
81
+ }