Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 148 additions & 104 deletions src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,124 +7,168 @@ const RISK_COLOR = {
high: pc.red,
} as const;

const CONF_TAG = {
high: pc.red(pc.bold("HIGH")),
medium: pc.yellow(pc.bold("MED")),
low: pc.dim(pc.bold("LOW")),
const CONF_BADGE = {
high: pc.bgRed(pc.white(pc.bold(" HIGH "))),
medium: pc.bgYellow(pc.black(pc.bold(" MED "))),
low: pc.bgBlack(pc.dim(" LOW ")),
} as const;

const ORDER = { high: 0, medium: 1, low: 2 } as const;

function date(utc: number): string {
return new Date(utc * 1000).toISOString().slice(0, 10);
const DEFAULT_WIDTH = 80;
const MAX_WIDTH = 120;

function getBoxWidth(): number {
const cols = process.stdout.columns;
if (!cols) return DEFAULT_WIDTH;
return Math.max(10, Math.min(cols - 4, MAX_WIDTH));
}
Comment on lines +21 to +25

function rule(): string {
return pc.dim("─".repeat(72));
function stripAnsi(s: string): string {
// Use RegExp constructor to bypass ESLint no-control-regex on literals
const ansiRegex = new RegExp(
"[\\u001b\\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]",
"g",
);
return s.replace(ansiRegex, "");
}

function sectionHead(label: string): string {
return pc.dim(`── ${label} ` + "─".repeat(Math.max(0, 68 - label.length)));
/** ANSI-aware word wrap. */
function wrap(text: string, width: number): string[] {
const lines: string[] = [];
const words = text.trim().split(/\s+/);
let current = "";
Comment on lines +38 to +40
let currentVisibleLen = 0;

for (const word of words) {
const wordVisibleLen = stripAnsi(word).length;

// Hard-wrap long unstyled tokens (e.g., URLs) to avoid overflowing the box.
if (stripAnsi(word) === word && wordVisibleLen > width) {
if (current) lines.push(current);
for (let i = 0; i < word.length; i += width) {
lines.push(word.slice(i, i + width));
}
Comment on lines +37 to +51
current = "";
currentVisibleLen = 0;
continue;
}

const space = current ? 1 : 0;
if (currentVisibleLen + space + wordVisibleLen > width) {
if (current) lines.push(current);
current = word;
currentVisibleLen = wordVisibleLen;
} else {
current = current ? current + " " + word : word;
currentVisibleLen += space + wordVisibleLen;
}
}
Comment on lines +43 to +66
if (current) lines.push(current);
return lines;
}
Comment on lines +36 to +69

function date(utc: number): string {
return new Date(utc * 1000).toISOString().slice(0, 10);
}

/** A 10-cell meter that fills with the risk color, e.g. ██████░░░░ */
function riskMeter(risk: AuditResult["overallRisk"]): string {
const filled = { low: 3, medium: 6, high: 10 }[risk];
return RISK_COLOR[risk]("█".repeat(filled)) + pc.dim("·".repeat(10 - filled));
const color = RISK_COLOR[risk];
return color("█".repeat(filled)) + pc.dim("░".repeat(10 - filled));
}

function wrapBlock(text: string, indent: string, width = 70): string {
const limit = Math.max(20, width - indent.length);
const lines: string[] = [];
for (const para of text.split(/\n+/)) {
let current = "";
for (const word of para.split(/\s+/)) {
if (!word) continue;
if (current && current.length + 1 + word.length > limit) {
lines.push(indent + current);
current = word;
} else {
current = current ? `${current} ${word}` : word;
}
}
if (current) lines.push(indent + current);
}
return lines.join("\n");
/** A boxed banner around a title line and any number of dim subtitle lines. */
function banner(title: string, subtitles: string[]): string[] {
const w = getBoxWidth();
const top = pc.dim("╭" + "─".repeat(w - 2) + "╮");
const bottom = pc.dim("╰" + "─".repeat(w - 2) + "╯");
return [top, " " + title, ...subtitles.map((s) => " " + s), bottom];
}

function findingBlock(f: Finding, n: number): string[] {
const out: string[] = [];
out.push(
` ${CONF_TAG[f.confidence]} ${pc.dim(`#${n}`)} ${pc.cyan(f.category)}`,
);
out.push(wrapBlock(pc.bold(f.claim), " "));
out.push("");
out.push(
` ${pc.dim("why")} ${wrapBlock(f.rationale, " ").trimStart()}`,
);
for (const e of f.evidence ?? []) {
out.push(
` ${pc.dim("┊")} ${pc.italic(`"${e.quote.replace(/\s+/g, " ").slice(0, 240)}"`)}`,
);
out.push(` ${pc.blue(pc.underline(e.permalink))}`);
}
out.push(
` ${pc.green("fix")} ${wrapBlock(f.remediation, " ").trimStart()}`,
);
out.push("");
return out;
/** Wrap content lines in a colored box with the severity badge in the top border. */
function findingBox(
f: Finding,
_index: number,
contentLines: string[],
Comment on lines +91 to +94
): string[] {
const color = RISK_COLOR[f.confidence];
const badge = CONF_BADGE[f.confidence];
const w = getBoxWidth();

// Top border: ┌─ [badge] ─── ┐
const dashes = Math.max(0, w - 10);
const top = color("┌─ ") + badge + color("─".repeat(dashes) + "┐");
const bottom = color("└" + "─".repeat(w - 2) + "┘");

const boxed = contentLines.map((line) => {
const visibleLen = stripAnsi(line).length;
const padding = Math.max(0, w - visibleLen - 4);
return color("│") + " " + line + " ".repeat(padding) + color(" │");
});

return [top, ...boxed, bottom];
}

export function renderText(r: AuditResult): string {
const out: string[] = [];
const counts = { high: 0, medium: 0, low: 0 };
for (const f of r.findings) counts[f.confidence] += 1;
const risk = r.overallRisk;

const w = getBoxWidth();
const innerW = w - 6;

out.push("");
out.push(` ${pc.bold("deanonymizer")} ${pc.dim("· exposure report")}`);
out.push(
` ${pc.dim(r.platformProfiles.map((p) => `${p.platform}:${p.username}`).join(" "))}` +
` ${pc.dim(`· ${r.itemCount} items`)}` +
(r.span
? ` ${pc.dim(`· ${date(r.span.firstUtc)} → ${date(r.span.lastUtc)}`)}`
: ""),
...banner(pc.bold("🔎 deanonymizer — exposure report"), [
pc.dim(`${r.username} · ${r.platforms.join(", ")}`),
pc.dim(
`${r.itemCount} items` +
(r.span
? ` · ${date(r.span.firstUtc)} → ${date(r.span.lastUtc)}`
: ""),
),
]),
);
out.push("");

const risk = r.overallRisk;
out.push(
` ${pc.dim("risk")} ${riskMeter(risk)} ${RISK_COLOR[risk](pc.bold(risk.toUpperCase()))}`,
` Overall risk ${riskMeter(risk)} ` +
RISK_COLOR[risk](pc.bold(risk.toUpperCase())),
);
out.push(
` ${pc.dim("findings")} ` +
`${counts.high ? pc.red(`${counts.high} high`) : pc.dim("0 high")}` +
` ${counts.medium ? pc.yellow(`${counts.medium} med`) : pc.dim("0 med")}` +
` ${counts.low ? `${counts.low} low` : pc.dim("0 low")}`,
` Findings ${pc.red(`${counts.high} high`)} · ` +
`${pc.yellow(`${counts.medium} medium`)} · ` +
`${pc.dim(`${counts.low} low`)}`,
);
out.push("");

// Identity block
out.push(sectionHead("identity"));
out.push("");
out.push(` ${pc.bold(r.identity.exactUser)}`);
out.push(wrapBlock(pc.dim(r.identity.rationale), " "));
out.push(` ${pc.bold("Exact user")} ${r.identity.exactUser}`);

const rationaleLines = wrap(r.identity.rationale, innerW - 10);
rationaleLines.forEach((line, i) => {
const prefix = i === 0 ? ` ${pc.dim("proof")} ` : ` `;
out.push(prefix + line);
});

if ((r.identity.publicProofUrls?.length ?? 0) > 0) {
out.push("");
for (const url of r.identity.publicProofUrls) {
out.push(` ${pc.dim("·")} ${pc.blue(pc.underline(url))}`);
out.push(` ${pc.dim("·")} ${pc.blue(pc.underline(url))}`);
}
}
out.push("");

// Direct identifiers (deterministic extraction — bypasses model)
// Direct Identifiers (from maintainer's new main)
const emails = r.directIdentifiers?.emails ?? [];
const handles = r.directIdentifiers?.socialHandles ?? [];
if (emails.length > 0 || handles.length > 0) {
out.push(sectionHead("direct identifiers extracted"));
out.push(pc.dim("── direct identifiers extracted ".padEnd(w, "─")));
out.push("");
Comment on lines +167 to 168
if (emails.length > 0) {
out.push(` ${pc.dim("emails")}`);
for (const e of emails) {
out.push(` ${pc.red("✉")} ${pc.bold(e)}`);
}
for (const e of emails) out.push(` ${pc.red("✉")} ${pc.bold(e)}`);
out.push("");
}
if (handles.length > 0) {
Expand All @@ -138,54 +182,54 @@ export function renderText(r: AuditResult): string {
}
out.push("");
}
out.push(
pc.dim(
" Pulled by regex (post-HTML-strip) from item bodies, commit author\n" +
" lines, and links scraped from the audited profile's external sites.\n" +
" These are concrete, citable leaks — scrub them first.",
),
);
out.push("");
}

// Summary
if (r.summary) {
out.push(sectionHead("summary"));
out.push("");
out.push(wrapBlock(r.summary, " "));
out.push("");
}
wrap(r.summary, w - 4).forEach((line) => out.push(" " + line));
out.push("");

if (r.findings.length === 0) {
out.push(rule());
out.push(pc.dim("─".repeat(w)));
out.push(
pc.green(" ✓ No identifying signals found in the analyzed window."),
);
return out.join("\n");
}

// Findings grouped by confidence
const sorted = [...r.findings].sort(
(a, b) => ORDER[a.confidence] - ORDER[b.confidence],
);

let currentGroup: Finding["confidence"] | null = null;
sorted.forEach((f, i) => {
if (f.confidence !== currentGroup) {
const label =
f.confidence === "high"
? "high-confidence findings"
: f.confidence === "medium"
? "medium-confidence findings"
: "low-confidence findings";
out.push(sectionHead(label));
out.push("");
currentGroup = f.confidence;
sorted.forEach((f: Finding, i) => {
const lines: string[] = [];
const headerText = `${pc.dim(`#${i + 1}`)} ${pc.cyan(f.category)} — ${pc.bold(f.claim)}`;
wrap(headerText, innerW).forEach((l) => lines.push(l));

Comment on lines +203 to +206
const whyLines = wrap(f.rationale, innerW - 6);
whyLines.forEach((l, idx) => {
const prefix = idx === 0 ? ` ${pc.dim("why")} ` : ` `;
lines.push(prefix + l);
});

for (const e of f.evidence ?? []) {
const quoteLines = wrap(`"${e.quote}"`, innerW - 6);
quoteLines.forEach((l, idx) => {
const prefix = idx === 0 ? ` ${pc.dim("┊")} ` : ` `;
lines.push(prefix + l);
});
lines.push(` ${pc.blue(pc.underline(e.permalink))}`);
Comment on lines +213 to +219
}
out.push(...findingBlock(f, i + 1));

const fixLines = wrap(f.remediation, innerW - 6);
fixLines.forEach((l, idx) => {
const prefix = idx === 0 ? ` ${pc.green("fix")} ` : ` `;
lines.push(prefix + l);
});

out.push(...findingBox(f, i, lines));
out.push("");
});

out.push(rule());
out.push(pc.dim("─".repeat(w)));
out.push(
pc.dim(
" Prioritize HIGH-confidence findings. Edit or delete the cited items,\n" +
Expand Down