@bigsteele/the-prospect 0.3.1 → 0.3.3

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.
@@ -85,6 +85,7 @@ export async function detectDatabase(repo) {
85
85
  * who may call; a bare `grant execute ... to service_role` narrows nothing.
86
86
  */
87
87
  const executeRevoked = new Set();
88
+ const definerBodies = [];
88
89
  let guard;
89
90
  // Two passes: every `alter table` and every `revoke` in the repository is
90
91
  // collected first, because a table is very often created in one migration
@@ -141,13 +142,18 @@ export async function detectDatabase(repo) {
141
142
  const stmt = stripComments(withComments);
142
143
  if (/create\s+(or\s+replace\s+)?policy/i.test(stmt)) {
143
144
  policies++;
144
- const name = /create\s+(?:or\s+replace\s+)?policy\s+("?[a-z0-9_]+"?)/i.exec(stmt)?.[1]?.replace(/"/g, "");
145
+ // A quoted name is the whole quoted string (0.3.2): `"anyone reads active
146
+ // plans"` was captured as `anyone`, and the finding was pinned to whichever
147
+ // table the file was named for. The subject now carries the table.
148
+ const m = /create\s+(?:or\s+replace\s+)?policy\s+(?:"([^"]+)"|([a-z0-9_]+))\s+on\s+([a-z0-9_."]+)/i.exec(stmt);
149
+ const name = m ? (m[1] ?? m[2]) : undefined;
150
+ const table = m ? qualify(m[3]) : undefined;
145
151
  // `to anon` hands the policy to unauthenticated callers. `to public`
146
152
  // is the same reach by another name, since anon is a member of public.
147
153
  if (name && /\bto\s+(anon|public)\b/i.test(stmt)) {
148
154
  findings.push({
149
155
  kind: "policy_reaches_anon",
150
- subject: name,
156
+ subject: table ? `${name} on ${table}` : name,
151
157
  file: f,
152
158
  note: "a policy granted to anon or public is readable by an unauthenticated caller. Deliberate for genuinely public rows, and worth one look per policy.",
153
159
  });
@@ -155,8 +161,14 @@ export async function detectDatabase(repo) {
155
161
  }
156
162
  if (/create\s+(or\s+replace\s+)?function/i.test(stmt) && /security\s+definer/i.test(stmt)) {
157
163
  definerFunctions++;
164
+ definerBodies.push({ fn: qualify(/create\s+(?:or\s+replace\s+)?function\s+([a-z0-9_."]+)\s*\(/i.exec(stmt)?.[1] ?? "?"), body: stmt });
158
165
  const name = /create\s+(?:or\s+replace\s+)?function\s+([a-z0-9_."]+)\s*\(/i.exec(stmt)?.[1];
159
- if (name && !CALLER_CHECK.test(withComments) && !executeRevoked.has(qualify(name))) {
166
+ // A TRIGGER FUNCTION CANNOT BE CALLED (0.3.2). Postgres refuses it:
167
+ // "trigger functions can only be called as triggers". Eight of them were
168
+ // reported as callable by anyone on the first deep read, because the
169
+ // shape matched SECURITY DEFINER without reading the return type.
170
+ const isTrigger = /returns\s+trigger\b/i.test(stmt);
171
+ if (name && !isTrigger && !CALLER_CHECK.test(withComments) && !executeRevoked.has(qualify(name))) {
160
172
  findings.push({
161
173
  kind: "definer_without_check",
162
174
  subject: qualify(name),
@@ -178,12 +190,22 @@ export async function detectDatabase(repo) {
178
190
  });
179
191
  }
180
192
  else if (!rlsForced.has(t)) {
181
- findings.push({
182
- kind: "rls_not_forced",
183
- subject: t,
184
- file: f,
185
- note: "row level security is enabled but not forced, so the table owner bypasses it. Forcing it is one line and closes the gap.",
186
- });
193
+ // FORCE ONLY MATTERS WHERE THE OWNER READS (0.3.2). FORCE binds the table
194
+ // owner, and clients never connect as the owner; the bypass is reachable
195
+ // only through a SECURITY DEFINER function that runs as the owner and reads
196
+ // the table. Thirty-nine tables were listed on the first deep read and every
197
+ // one was refuted on exactly that point. So: the table is a finding when a
198
+ // definer function names it, and the note says which one.
199
+ const bare = t.split(".").pop();
200
+ const reader = definerBodies.find((d) => new RegExp(`\\b(?:${t.replace(".", "\\.")}|${bare})\\b`, "i").test(d.body.replace(/create\s+(?:or\s+replace\s+)?function\s+[a-z0-9_."]+/i, "")));
201
+ if (reader) {
202
+ findings.push({
203
+ kind: "rls_not_forced",
204
+ subject: t,
205
+ file: f,
206
+ note: `row level security is enabled but not forced, and \`${reader.fn}\` runs as the table owner and reads it, so the owner's bypass is reachable through that function. Forcing it is one line.`,
207
+ });
208
+ }
187
209
  }
188
210
  }
189
211
  return {
@@ -193,6 +215,8 @@ export async function detectDatabase(repo) {
193
215
  definer_functions: definerFunctions,
194
216
  definer_execute_revoked: executeRevoked.size,
195
217
  guard,
196
- findings: findings.sort((a, b) => a.kind.localeCompare(b.kind) || a.subject.localeCompare(b.subject)).slice(0, 60),
218
+ // Every finding, not the first sixty: the deep read rules on each by id, and a cap
219
+ // hid 87 of 147 on the first real repository while the report said "60 shapes".
220
+ findings: findings.sort((a, b) => a.kind.localeCompare(b.kind) || a.subject.localeCompare(b.subject)).slice(0, 400),
197
221
  };
198
222
  }
package/dist/index.d.ts CHANGED
@@ -45,5 +45,5 @@ export interface Prospect {
45
45
  entrypoints: number;
46
46
  };
47
47
  }
48
- export declare const VERSION = "0.3.1";
48
+ export declare const VERSION = "0.3.3";
49
49
  export declare function runProspect(root: string): Promise<Prospect>;
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import { scoreProspect } from "./score.js";
37
37
  export { toMarkdown, secretShaped } from "./report.js";
38
38
  export { checkReport } from "./check.js";
39
39
  export { checkVerdicts, rescore, showMath, findingIds } from "./verdicts.js";
40
- export const VERSION = "0.3.1";
40
+ export const VERSION = "0.3.3";
41
41
  export async function runProspect(root) {
42
42
  const repo = await openRepo(root);
43
43
  const runtime = runtimeCode(repo.files);
@@ -113,8 +113,12 @@ export async function runProspect(root) {
113
113
  d.id = `dup:${i + 1}:${d.files[0] ?? ""}`; });
114
114
  for (const d of deadReading.dead)
115
115
  d.id = `dead:${d.file}`;
116
+ // Only the multiplying shape is a finding. One paid call per request is what a
117
+ // request handler IS; the first deep read ruled on twenty-four of them one by
118
+ // one and refuted every one. They stay in the table as inventory, unnamed.
116
119
  for (const c of costs)
117
- c.id = `cost:${c.file}:${c.line.split(":")[0]}`;
120
+ if (c.shape === "per-row")
121
+ c.id = `cost:${c.file}:${c.line.split(":")[0]}`;
118
122
  for (const f of database.findings)
119
123
  f.id = `db:${f.kind}:${f.subject}`;
120
124
  // Last, and after every detector, so the ledger describes the run that just
package/dist/report.js CHANGED
@@ -285,8 +285,14 @@ export function toMarkdown(p) {
285
285
  }
286
286
  if (accidental.length) {
287
287
  L.push(`### The same code in more than one place`, "");
288
- for (const d of accidental.slice(0, 10)) {
289
- L.push(`- ${d.lines} matching lines across ${d.files.join(", ")} - opens with \`${d.opens_with}\``);
288
+ // THE REAL CLUSTERS FIRST (0.3.3). Sorted by size, the top ten on a repository
289
+ // full of adapter templates were all parallel - already explained one section
290
+ // up, never charged - and the four accidental clusters the score actually
291
+ // counted fell off the list. The reader was shown what is fine and hidden
292
+ // what is not. Accidental clusters lead; parallel ones follow, marked.
293
+ const ordered = [...accidental].sort((x, y) => Number(!!x.parallel) - Number(!!y.parallel) || y.lines - x.lines);
294
+ for (const d of ordered.slice(0, 10)) {
295
+ L.push(`- ${d.parallel ? "(parallel by design) " : ""}${d.lines} matching lines across ${d.files.join(", ")} - opens with \`${d.opens_with}\``);
290
296
  }
291
297
  const deliberate = p.duplicates.length - accidental.length;
292
298
  if (deliberate > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigsteele/the-prospect",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A prospector's read of your codebase and your market. Digs the ground you own: every dependency that does no work, every vendor you pay twice, every subsystem you built by hand where a rail now exists. Then surveys the territory: what your industry ships by API that your code still does the hard way. Every suggestion stands on three legs - a fact read from your code, a fact researched from your market with a source and a date, and the thing your product exists to do. Standalone: one npx, no other scan required.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",