spillage 0.9.0__py3-none-any.whl

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.
spillage/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Find the secrets your coding agents spilled."""
2
+
3
+ __version__ = "0.9.0"
spillage/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,254 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>spillage report</title>
8
+ <style>
9
+ :root {
10
+ --bg: #0e0f13; --panel: #16181f; --panel-2: #1c1f28; --line: #262a35; --text: #e9e7e2; --muted: #8b8f9c;
11
+ --accent: #ff6b4a; --accent-soft: rgba(255,107,74,.14);
12
+ --critical: #ff4d5e; --high: #d86bff; --medium: #ffc24a; --low: #5aa9ff; --ok: #3ddc97;
13
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
14
+ --sans: ui-sans-serif, -apple-system, "Segoe UI", Inter, Roboto, sans-serif;
15
+ }
16
+ @media (prefers-color-scheme: light) {
17
+ :root { --bg: #f7f5f1; --panel: #ffffff; --panel-2: #f1eee8; --line: #e4e0d8; --text: #16171c; --muted: #6b6f7a;
18
+ --accent-soft: rgba(255,107,74,.12); --accent: #e8502e;
19
+ --critical: #d92d3f; --high: #9b3fd1; --medium: #b07400; --low: #2a6fd6; --ok: #139e62; }
20
+ }
21
+ * { box-sizing: border-box; }
22
+ [hidden] { display: none !important; }
23
+ html { background: var(--bg); }
24
+ body { margin: 0; font: 15px/1.55 var(--sans); color: var(--text); background: var(--bg); -webkit-font-smoothing: antialiased; }
25
+ .wrap { max-width: 1040px; margin: 0 auto; padding: 40px 20px 80px; }
26
+ header { display: flex; align-items: center; gap: 14px; margin-bottom: 36px; }
27
+ header svg { flex: none; }
28
+ header h1 { font-size: 18px; margin: 0; letter-spacing: -.01em; }
29
+ header .meta { color: var(--muted); font-size: 13px; }
30
+ header .spacer { flex: 1; }
31
+ .hero { display: grid; grid-template-columns: 1.1fr 1fr; gap: 20px; margin-bottom: 20px; }
32
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 22px; }
33
+ .big { font-size: 64px; font-weight: 700; letter-spacing: -.04em; line-height: 1; }
34
+ .big small { font-size: 18px; font-weight: 500; letter-spacing: 0; color: var(--muted); margin-left: 6px; }
35
+ .bar { display: flex; height: 10px; border-radius: 99px; overflow: hidden; background: var(--panel-2); margin: 22px 0 12px; }
36
+ .bar span { height: 100%; width: 0; transition: width 1.1s cubic-bezier(.2,.8,.2,1); }
37
+ .legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; color: var(--muted); }
38
+ .legend b { color: var(--text); font-weight: 600; }
39
+ .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: 1px; }
40
+ h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin: 0 0 14px; font-weight: 600; }
41
+ .origins div { display: grid; grid-template-columns: 150px 1fr 28px; align-items: center; gap: 10px; font-size: 13px; margin: 9px 0; }
42
+ .origins i { display: block; height: 8px; border-radius: 99px; background: var(--accent); width: 0; transition: width 1s cubic-bezier(.2,.8,.2,1) .2s; }
43
+ .origins em { font-style: normal; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
44
+ .timeline { margin-bottom: 20px; }
45
+ .timeline svg { width: 100%; height: 110px; display: block; overflow: visible; }
46
+ .timeline text { fill: var(--muted); font: 11px var(--sans); }
47
+ .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 34px 0 14px; }
48
+ .chip { border: 1px solid var(--line); background: var(--panel); color: var(--text); border-radius: 99px; padding: 6px 13px; font: 13px var(--sans); cursor: pointer; transition: all .15s; }
49
+ .chip:hover { border-color: var(--muted); }
50
+ .chip[aria-pressed="true"] { background: var(--text); color: var(--bg); border-color: var(--text); }
51
+ .search { flex: 1; min-width: 180px; border: 1px solid var(--line); background: var(--panel); color: var(--text); border-radius: 99px; padding: 7px 15px; font: 13px var(--sans); outline: none; }
52
+ .search:focus { border-color: var(--accent); }
53
+ .finding { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 18px 20px; margin-bottom: 12px; opacity: 0; transform: translateY(8px); animation: rise .5s cubic-bezier(.2,.8,.2,1) forwards; position: relative; overflow: hidden; }
54
+ .finding::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--sev); }
55
+ @keyframes rise { to { opacity: 1; transform: none; } }
56
+ .top { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px; }
57
+ .sev { font: 600 11px var(--mono); letter-spacing: .06em; text-transform: uppercase; padding: 3px 8px; border-radius: 6px; color: var(--sev); background: color-mix(in srgb, var(--sev) 14%, transparent); }
58
+ .name { font-weight: 650; }
59
+ .masked { font: 13px var(--mono); background: var(--panel-2); padding: 2px 8px; border-radius: 6px; }
60
+ .fp { font: 12px var(--mono); color: var(--muted); margin-left: auto; cursor: pointer; border: 0; background: none; padding: 0; }
61
+ .fp:hover { color: var(--text); }
62
+ .facts { display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--muted); font-size: 13px; margin: 10px 0 0; }
63
+ .facts b { color: var(--text); font-weight: 500; }
64
+ details { margin-top: 12px; }
65
+ summary { cursor: pointer; color: var(--muted); font-size: 13px; list-style: none; }
66
+ summary::-webkit-details-marker { display: none; }
67
+ summary::before { content: "▸ "; }
68
+ details[open] summary::before { content: "▾ "; }
69
+ .locs { margin: 8px 0 0; padding: 0; list-style: none; font: 12px/1.7 var(--mono); color: var(--muted); max-height: 240px; overflow: auto; }
70
+ .locs span { color: var(--text); }
71
+ .rotate { display: inline-flex; align-items: center; gap: 6px; margin-top: 14px; font-size: 13px; font-weight: 600; color: var(--accent); text-decoration: none; }
72
+ .rotate:hover { text-decoration: underline; }
73
+ .empty { text-align: center; padding: 70px 20px; }
74
+ .empty .big { color: var(--ok); }
75
+ .steps { margin-top: 34px; }
76
+ .steps ol { margin: 0; padding-left: 20px; }
77
+ .steps li { margin: 8px 0; }
78
+ code { font: 13px var(--mono); background: var(--panel-2); padding: 2px 6px; border-radius: 6px; }
79
+ footer { margin-top: 40px; color: var(--muted); font-size: 12px; text-align: center; }
80
+ .toast { position: fixed; bottom: 24px; left: 50%; transform: translate(-50%, 20px); background: var(--text); color: var(--bg); padding: 8px 16px; border-radius: 99px; font-size: 13px; opacity: 0; transition: all .25s; pointer-events: none; }
81
+ .toast.on { opacity: 1; transform: translate(-50%, 0); }
82
+ @media (max-width: 760px) { .hero { grid-template-columns: 1fr; } .big { font-size: 52px; } .origins div { grid-template-columns: 120px 1fr 24px; } }
83
+ @media (prefers-reduced-motion: reduce) { *, *::before { animation: none !important; transition: none !important; } .finding { opacity: 1; transform: none; } }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <div class="wrap">
88
+ <header>
89
+ <svg width="34" height="34" viewBox="0 0 64 64" aria-hidden="true"><path d="M32 6c9 13 18 23 18 34a18 18 0 0 1-36 0C14 29 23 19 32 6z" fill="#ff6b4a"/><path d="M24 42a8 8 0 0 0 8 8" stroke="#fff" stroke-opacity=".55" stroke-width="4" fill="none" stroke-linecap="round"/></svg>
90
+ <div><h1>spillage report</h1><div class="meta" id="meta"></div></div>
91
+ <div class="spacer"></div>
92
+ </header>
93
+ <main id="app"></main>
94
+ <footer>Generated by spillage __VERSION__. Values are masked; the full secrets never leave the log files.</footer>
95
+ </div>
96
+ <div class="toast" id="toast"></div>
97
+ <script id="data" type="application/json">__DATA__</script>
98
+ <script>
99
+ (() => {
100
+ const D = JSON.parse(document.getElementById("data").textContent);
101
+ const SEV = ["critical", "high", "medium", "low"];
102
+ const ORIGIN = {"prompt": "Pasted in a prompt", "tool-output": "Printed by a tool", "assistant": "Repeated by the model",
103
+ "file-snapshot": "File backups", "history": "Prompt history", "config": "Agent settings",
104
+ "other": "Session metadata"};
105
+ const app = document.getElementById("app");
106
+ const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
107
+ const css = (name) => `var(--${name})`; // follows a theme switch, no style recalc
108
+ let toastTimer = 0;
109
+ const toast = (msg, ms = 1400) => { const t = document.getElementById("toast"); t.textContent = msg; t.classList.add("on"); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("on"), ms); };
110
+ const copy = (text, msg) => { (navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(() => toast(msg), () => toast(text, 6000)); };
111
+
112
+ // a scan that could not read a file must not look complete
113
+ const unread = (D.summary.errors || []).length;
114
+ const unreadNote = unread ? `${unread} file${unread === 1 ? "" : "s"} could not be scanned` : "";
115
+ document.getElementById("meta").textContent =
116
+ `${D.generated} · ${D.summary.files} files (${D.size}) from ${D.agents.join(", ") || "nothing"} · ${D.summary.seconds}s`
117
+ + (unread ? ` · ${unreadNote}` : "");
118
+
119
+ const F = D.findings;
120
+ if (!F.length) {
121
+ const e = el("div", "card empty");
122
+ const verdict = unread ? `No secrets in the files that could be read. ${unreadNote}.`
123
+ : D.summary.files ? "Nothing spilled. No secrets in your agent logs." : "No agent logs found.";
124
+ e.append(el("div", "big", "0"), el("p", null, verdict));
125
+ app.append(e);
126
+ return;
127
+ }
128
+
129
+ // ---- hero ------------------------------------------------------------------------------
130
+ const hero = el("section", "hero");
131
+ const left = el("div", "card");
132
+ const big = el("div", "big", String(F.length));
133
+ const noun = el("small", null, F.length === 1 ? "secret spilled" : "secrets spilled");
134
+ big.append(noun);
135
+ left.append(big);
136
+ const bar = el("div", "bar"), legend = el("div", "legend");
137
+ SEV.forEach(s => {
138
+ const n = D.summary[s]; const span = el("span"); span.style.background = css(s); span.dataset.w = (n / F.length * 100) + "%"; bar.append(span);
139
+ const item = el("span"); const dot = el("i", "dot"); dot.style.background = css(s); const b = el("b", null, String(n));
140
+ item.append(dot, b, " " + s); legend.append(item);
141
+ });
142
+ left.append(bar, legend);
143
+ const right = el("div", "card origins");
144
+ right.append(el("h2", null, "How they got there"));
145
+ const counts = {};
146
+ F.forEach(f => f.origins.forEach(o => counts[o] = (counts[o] || 0) + 1));
147
+ const maxO = Math.max(1, ...Object.values(counts));
148
+ Object.keys(ORIGIN).filter(o => counts[o]).forEach(o => {
149
+ const row = el("div"); const track = el("div"); const fill = el("i"); fill.dataset.w = (counts[o] / maxO * 100) + "%";
150
+ track.append(fill); row.append(el("span", null, ORIGIN[o]), track, el("em", null, String(counts[o]))); right.append(row);
151
+ });
152
+ hero.append(left, right);
153
+ app.append(hero);
154
+
155
+ // ---- timeline ---------------------------------------------------------------------------
156
+ const dated = F.filter(f => f.first_seen && Number.isFinite(Date.parse(f.first_seen)));
157
+ if (dated.length) {
158
+ const card = el("section", "card timeline"); card.append(el("h2", null, "When they leaked"));
159
+ const ns = "http://www.w3.org/2000/svg";
160
+ const svg = document.createElementNS(ns, "svg"); svg.setAttribute("viewBox", "0 0 1000 110"); svg.setAttribute("preserveAspectRatio", "none");
161
+ const t = dated.map(f => Date.parse(f.first_seen)); let lo = Math.min(...t), hi = Math.max(...t);
162
+ if (hi - lo < 86400000) { lo -= 43200000; hi += 43200000; }
163
+ const x = v => 20 + (v - lo) / (hi - lo) * 960;
164
+ const axis = document.createElementNS(ns, "line"); axis.setAttribute("x1", 20); axis.setAttribute("x2", 980); axis.setAttribute("y1", 70); axis.setAttribute("y2", 70);
165
+ axis.style.stroke = css("line"); axis.setAttribute("stroke-width", 2); svg.append(axis);
166
+ [lo, (lo + hi) / 2, hi].forEach((v, i) => {
167
+ const tx = document.createElementNS(ns, "text"); tx.setAttribute("x", x(v)); tx.setAttribute("y", 100);
168
+ tx.setAttribute("text-anchor", ["start", "middle", "end"][i]); tx.textContent = new Date(v).toISOString().slice(0, 10); svg.append(tx);
169
+ });
170
+ dated.forEach((f, i) => {
171
+ const c = document.createElementNS(ns, "circle"); const r = 5 + Math.min(9, Math.sqrt(f.occurrences) * 2);
172
+ c.setAttribute("cx", x(Date.parse(f.first_seen))); c.setAttribute("cy", 70 - (i % 3) * 16); c.setAttribute("r", 0);
173
+ c.style.fill = css(f.severity); c.setAttribute("fill-opacity", ".85");
174
+ const title = document.createElementNS(ns, "title"); title.textContent = `${f.name} · ${f.masked} · ${f.first_seen.slice(0, 10)}`; c.append(title);
175
+ svg.append(c);
176
+ setTimeout(() => { c.style.transition = "r .6s cubic-bezier(.2,.8,.2,1)"; c.setAttribute("r", r); }, 300 + i * 60);
177
+ });
178
+ card.append(svg); app.append(card);
179
+ }
180
+
181
+ // ---- filters ----------------------------------------------------------------------------
182
+ const controls = el("div", "controls");
183
+ const state = { sev: null, agent: null, q: "" };
184
+ const chips = [];
185
+ const chip = (label, key, value) => {
186
+ const c = el("button", "chip", label); c.setAttribute("aria-pressed", "false");
187
+ c.onclick = () => { state[key] = state[key] === value ? null : value; chips.forEach(o => o.b.setAttribute("aria-pressed", String(state[o.k] === o.v))); render(); };
188
+ chips.push({ b: c, k: key, v: value }); return c;
189
+ };
190
+ SEV.filter(s => D.summary[s]).forEach(s => controls.append(chip(s, "sev", s)));
191
+ const agents = [...new Set(F.flatMap(f => f.agents))];
192
+ if (agents.length > 1) agents.forEach(a => controls.append(chip(D.labels[a] || a, "agent", a)));
193
+ const search = el("input", "search"); search.placeholder = "Filter by name, file, project…"; search.oninput = () => { state.q = search.value.toLowerCase(); render(); };
194
+ controls.append(search);
195
+ app.append(controls);
196
+ const list = el("section"); app.append(list);
197
+
198
+ const card = (f, i) => {
199
+ const c = el("article", "finding"); c.style.setProperty("--sev", css(f.severity)); c.style.animationDelay = animate ? (Math.min(i, 12) * 45) + "ms" : "0ms"; if (!animate) c.style.animation = "none", c.style.opacity = 1, c.style.transform = "none";
200
+ const top = el("div", "top");
201
+ const fp = el("button", "fp", f.fingerprint); fp.title = "Copy the ignore command"; fp.onclick = () => copy(`spillage ignore ${f.fingerprint}`, "Ignore command copied");
202
+ top.append(el("span", "sev", f.severity), el("span", "name", f.name), el("code", "masked", f.masked), fp);
203
+ const facts = el("div", "facts");
204
+ const fact = (k, v) => { const s = el("span", null, k + " "); s.append(el("b", null, v)); facts.append(s); };
205
+ fact("seen", `${f.occurrences}×`); fact("sessions", String(f.sessions)); fact("agents", f.agents.map(a => D.labels[a] || a).join(", "));
206
+ if (f.first_seen) fact("first", f.first_seen.slice(0, 10));
207
+ fact("how", f.origins.map(o => (ORIGIN[o] || o).toLowerCase()).join(", "));
208
+ c.append(top, facts);
209
+ const det = el("details"); det.append(el("summary", null, `${f.locations.length} location${f.locations.length === 1 ? "" : "s"}`));
210
+ const ul = el("ul", "locs");
211
+ f.locations.forEach(l => { const li = el("li"); li.append(el("span", null, l.file + (l.line ? ":" + l.line : "")), ` ${l.origin}${l.project ? " · " + l.project : ""}`); ul.append(li); });
212
+ det.append(ul); c.append(det);
213
+ if (f.rotate_url && /^https:\/\//.test(f.rotate_url)) {
214
+ const a = el("a", "rotate", `Rotate at ${new URL(f.rotate_url).host} →`); a.href = f.rotate_url; a.target = "_blank"; a.rel = "noopener noreferrer"; c.append(a);
215
+ }
216
+ return c;
217
+ };
218
+ // Cards are built once; filtering only hides and shows them.
219
+ let animate = true;
220
+ const hays = F.map(f => [f.name, f.provider, f.masked, f.fingerprint, ...f.locations.map(l => l.file + " " + (l.project || ""))].join(" ").toLowerCase());
221
+ const cards = F.map((f, i) => card(f, i));
222
+ animate = false;
223
+ const empty = el("p", "legend", "Nothing matches.");
224
+ list.append(...cards, empty);
225
+ const render = () => {
226
+ let shown = 0;
227
+ F.forEach((f, i) => {
228
+ const on = (!state.sev || f.severity === state.sev) && (!state.agent || f.agents.includes(state.agent)) && (!state.q || hays[i].includes(state.q));
229
+ cards[i].hidden = !on; shown += on;
230
+ });
231
+ empty.hidden = shown > 0;
232
+ };
233
+ render();
234
+
235
+ const steps = el("section", "card steps"); steps.append(el("h2", null, "What now"));
236
+ const ol = el("ol");
237
+ const li1 = el("li"); li1.append(el("b", null, "Rotate the keys above. "), "They reached the model provider when the conversation happened; deleting the logs doesn't undo that.");
238
+ const li2 = el("li"); li2.append("Run ", el("code", null, "spillage scrub"), " to remove them from the files on disk.");
239
+ const li3 = el("li"); li3.append("Run ", el("code", null, "spillage guard install"), " so the next one gets blocked before it's sent.");
240
+ ol.append(li1, li2, li3);
241
+ steps.append(ol); app.append(steps);
242
+
243
+ // ---- motion ---------------------------------------------------------------------------
244
+ requestAnimationFrame(() => requestAnimationFrame(() => {
245
+ document.querySelectorAll("[data-w]").forEach(e => e.style.width = e.dataset.w);
246
+ if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
247
+ const start = performance.now(), total = F.length, dur = 900;
248
+ const tick = now => { const k = Math.max(0, Math.min(1, (now - start) / dur)); big.firstChild.textContent = String(Math.round(total * (1 - Math.pow(1 - k, 3)))); if (k < 1) requestAnimationFrame(tick); };
249
+ requestAnimationFrame(tick);
250
+ }));
251
+ })();
252
+ </script>
253
+ </body>
254
+ </html>