Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 3 additions & 1 deletion AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ This versioned reference ships inside `@patchstack/connect` and documents each s
## Package and command behavior

- Package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. `npm view @patchstack/connect` shows the live registry metadata.
- It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env var values, no file paths, no git history. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.)
- **What is sent to Patchstack is the dependency list only** — read from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — package names + versions, for vulnerability matching. No source code, no env var values, no file paths, no git history is ever transmitted. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.)
- **One command reads source files, locally:** `map` (see below) parses your server source to report your app's attack surface. It runs only when you invoke it, prints to stdout, and transmits nothing. No other command reads source (`protect` writes guard files but does not analyze your code).
- **`scan` makes one source edit, and only after a successful post:** it adds (or updates) the disclosure widget's `<script>` tag in the project's root HTML shell — the first of `index.html`, `public/index.html`, or `src/app.html` that exists. It touches no other file, never edits on `--dry-run` or after a failed post, leaves any pre-existing manual widget tag untouched, and is disabled entirely by `"widget": false` in `.patchstackrc.json`. `mark-build` writes to build output only (`dist/`, `build/`, `out/`, `.output/public`), never to source. `guide`, `status`, and `init` write nothing except `init`'s own `.patchstackrc.json`.
- **`setup` runs `scan`, then `protect`, then edits `package.json` scripts:** provisioning happens first so the runtime guard can bake the real site UUID. It verifies the resulting framework seam, preserves existing commands, adds `scan` after dependency installs and before builds, adds `mark-build` after builds, and uses a direct build chain for Bun. It never runs the project build. If the widget or runtime guard needs a framework-specific manual merge, it prints the exact remaining step instead of overwriting user code.
- The package also exposes **`protect`** directly (runtime exploit guard; its templates live under `dist/protect/`). `setup` invokes it automatically; `scan`, `guide`, `status`, and `mark-build` do not. It writes only local files and auto-wires known stacks — **TanStack Start + Supabase** (patches the Supabase client + `src/start.ts`), **Next.js** (scaffolds `middleware.ts`), **SvelteKit** (`src/hooks.server.ts`), **Astro** (`src/middleware.ts`), **Nuxt** (`server/middleware/`), **NestJS** (`app.use(patchstackMiddleware)` in the bootstrap), **Fastify** (`app.register(patchstackFastify)`), and **Express** (`app.use(patchstackMiddleware)`). On **any other stack** it scaffolds a framework-agnostic guard under `src/patchstack/` and prints a wiring plan — then you finish the install by importing that guard into your server entry (`protectFetch(handler)` for a Web-Fetch server, or `app.use(patchstackMiddleware)` for Node/Express) and running `patchstack-connect protect --check` to confirm it is wired (exit 1 until it is). Passing `--demo` seeds a broad sample rule set (for demonstrations, not production).
- **`demo node-serialize` is an explicit production-backed walkthrough.** It requires `node-serialize@0.0.4` to already be present in the lockfile; it does not install the vulnerable dependency. It runs the same production `scan`, polls the configured site's public Pulse rules endpoint until rule `18843` is served, runs `protect`, verifies the generated guard, and prints exploit/benign test requests. It writes the same manifest/widget and guard files as those underlying commands. It does not start/restart the app and does not send the printed requests.
- **`map` is a local, read-only analysis command.** It walks the project's server source (skipping `node_modules`, build output and dot-directories; it does not follow symlinks out of the project unless you pass `--follow-symlinks`), parses it with the project's **own** `typescript`, and prints JSON describing the attack surface: entry points, the inputs each reads, the sinks they can reach (database / file system / process / outbound HTTP) with the npm package behind each, and evidence-backed input→sink flows marked `precise` or `heuristic`. Static analysis is best-effort, so the output reports the *detected* surface with coverage counters — not a completeness guarantee. It writes nothing (except the file you name with `--out`), contacts nothing, and is never invoked by `scan`, `setup`, `guide`, `protect`, or `mark-build`.
- **`demo-guide node-serialize` is the read-only companion.** It checks the Host-created site configuration and vulnerable lockfile entry, explains the complete local prepare/run/restart/prove/cleanup sequence, and prints the next exact command. It does not require a deployment and does not change files or contact Patchstack.
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ patchstack-connect protect Install/reconcile the always-
guard. Auto-wires supported server stacks;
use --check to verify or --demo for local rules.
Also run by setup; never run by scan/guide/mark-build.
patchstack-connect map [--dir p] [--out f] Print a JSON map of this project's attack
surface: server entry points, the inputs each
reads, the sinks they can reach (database, file
system, process, outbound HTTP) and the npm
package behind each sink. READS YOUR SOURCE
FILES locally and parses them with the
project's own TypeScript; writes nothing except
--out, and posts nothing. Never run by
scan/setup/guide/protect — run it yourself.
patchstack-connect demo node-serialize Production-backed walkthrough: require
node-serialize@0.0.4, scan it, wait for live
rule 18843, install + verify the runtime guard,
Expand Down Expand Up @@ -189,7 +198,7 @@ Lower-level pieces are also exported: `scanLockfile`, `buildWirePayload`, `postM
}
```

That's the entire payload. No source code, no environment variable values, no file paths — just the package names and versions from your lockfile. Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed. (`mark-build` separately stamps built HTML with a stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL` — never their values.)
That's the entire payload. No source code, no environment variable values, no file paths — just the package names and versions from your lockfile. (The `map` command reads source files locally to report your attack surface, but transmits nothing.) Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed. (`mark-build` separately stamps built HTML with a stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL` — never their values.)

## Supported lockfiles

Expand Down
48 changes: 47 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
renderGuideChecklist,
} from './guide.js';
import { runProtect, runVerify } from './protect/install/index.js';
import { buildInputMap } from './map/index.js';
import { setupProtection, wireBuildScripts } from './setup.js';
import { detectStack, type StackDescriptor } from './stack.js';
import { PatchstackError } from './types.js';
Expand All @@ -56,6 +57,14 @@ Usage:
manage the widget, install + verify runtime
protection, and wire dependency/build scans.
Never runs the project build
patchstack-connect map [--dir <p>] [--out <f>] Map the app's attack surface: entry points, the
inputs each reads, the sinks it can reach, and
evidence-backed input→sink flows (each marked
precise or heuristic). Best-effort static
analysis — reports the DETECTED surface, with
coverage counters. Prints JSON (--out writes a
file; --follow-symlinks leaves the project dir).
Uses the app's own TypeScript
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
with an existing site UUID
patchstack-connect status [options] Show current configuration and whether the
Expand Down Expand Up @@ -128,7 +137,7 @@ Examples:
npx @patchstack/connect demo-guide node-serialize
`;

const VALUE_FLAGS = new Set(['site-uuid', 'endpoint', 'dir', 'url']);
const VALUE_FLAGS = new Set(['site-uuid', 'endpoint', 'dir', 'url', 'out']);

interface ParsedArgs {
command: string | null;
Expand Down Expand Up @@ -193,6 +202,41 @@ async function runInit(args: ParsedArgs): Promise<number> {
return 0;
}

async function runMap(args: ParsedArgs): Promise<number> {
const cwd = getStringFlag(args.flags, 'dir') ?? process.cwd();
const { map, error } = await buildInputMap(cwd, {
followSymlinks: args.flags.get('follow-symlinks') === true,
});
if (!map) {
console.error(`patchstack: ${error}`);
return 1;
}
// Human summary → stderr; the JSON → stdout (so it can be piped / written). Report PRECISE flows
// separately from the inventories: only a precise flow is evidence that an input reaches a sink.
const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
const precise = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => f.confidence === 'precise').length, 0);
const c = map.coverage;
console.error(
`patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ` +
`${precise} proven input→sink flow(s) [${map.framework}].`,
);
console.error(
`patchstack: ${c.filesParsed}/${c.filesDiscovered} file(s) parsed` +
(c.filesSkipped ? `, ${c.filesSkipped} skipped` : '') +
`. DETECTED surface only — static analysis is best-effort; unproven pairs are marked "heuristic".`,
);
const json = JSON.stringify(map, null, 2);
const out = getStringFlag(args.flags, 'out');
if (out) {
writeFileSync(out, json);
console.error(`patchstack: wrote ${out}`);
} else {
console.log(json);
}
return 0;
}

async function runScan(
args: ParsedArgs,
options: { showRemainingSetup?: boolean } = {},
Expand Down Expand Up @@ -771,6 +815,8 @@ async function main(): Promise<number> {
return runGuide(args);
case 'setup':
return runSetup(args);
case 'map':
return runMap(args);
default:
console.error(`Unknown command: ${args.command}\n`);
console.error(HELP);
Expand Down
Loading
Loading