@patchstack/connect 0.3.31 → 0.3.32
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.
- package/AGENT-INSTALL.md +175 -18
- package/README.md +1 -2
- package/dist/{chunk-LLKP5EJS.js → chunk-3G2I6QL6.js} +1 -1
- package/dist/chunk-3G2I6QL6.js.map +1 -0
- package/dist/cli.js +95 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +2645 -1417
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +316 -0
- package/dist/protect.d.ts +134 -18
- package/dist/protect.edge.js +2723 -1477
- package/dist/protect.edge.js.map +3 -4
- package/dist/protect.js +2645 -1421
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-KNZQBC4V.js → refresh-manifest-SWCPQ52Z.js} +2 -2
- package/dist/refresh-manifest-SWCPQ52Z.js.map +1 -0
- package/package.json +44 -11
- package/dist/chunk-LLKP5EJS.js.map +0 -1
- package/dist/refresh-manifest-KNZQBC4V.js.map +0 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -144,18 +144,112 @@ would have stopped while it is still in dry-run. Two separate paths, with differ
|
|
|
144
144
|
token; the `apiKey` itself is not sent to the log endpoint. Disable with `PATCHSTACK_TELEMETRY=off`, or
|
|
145
145
|
`reportFirewallLog: false` in `createProtection`.
|
|
146
146
|
- **Every rule that matched** goes to `monitor/pulse/detections/<your site uuid>` — including matches that
|
|
147
|
-
blocked, which are reported on both paths.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
blocked, which are reported on both paths. It exists because a rule carrying `dry-run` blocks nothing, so
|
|
148
|
+
without it nothing distinguishes a rule that is protecting from one that is quietly wrong.
|
|
149
|
+
|
|
150
|
+
**This is on by default for a site enrolled with Patchstack that is running Patchstack-delivered rules**,
|
|
151
|
+
and off otherwise. Specifically, it requires all of: a provisioned site UUID, rules that came from
|
|
152
|
+
Patchstack rather than from a local bundle, and a resolvable credential. A local install, or a guard
|
|
153
|
+
running its own `rules`, sends nothing.
|
|
154
|
+
|
|
155
|
+
Switch it off with **`PATCHSTACK_REPORT_DETECTIONS=0`**, or `reportDetections: false` in
|
|
156
|
+
`createProtection`, or `PATCHSTACK_TELEMETRY=off` which covers all telemetry. `reportDetections` is an
|
|
157
|
+
opt-out only — passing `true` cannot switch reporting on for a site that is not enrolled.
|
|
158
|
+
|
|
159
|
+
`protection.detectionReporting` names the current state locally, so a guard that is not reporting says
|
|
160
|
+
which reason applies: `on`, `disabled-by-config`, `disabled-by-telemetry-opt-out`, `not-enrolled`,
|
|
161
|
+
`no-managed-rules`, or `unavailable-no-credential`.
|
|
162
|
+
|
|
163
|
+
**How the state reaches Patchstack, and what that costs on the network.** The state travels as a header
|
|
164
|
+
on the rules request the guard already makes — no extra request for it. Two of the six never travel:
|
|
165
|
+
`not-enrolled` makes no site-addressed request at all, and `unavailable-no-credential` cannot produce an
|
|
166
|
+
authenticated one, and the header is withheld from unauthenticated requests. Those two are local
|
|
167
|
+
diagnostics only.
|
|
168
|
+
|
|
169
|
+
There is one case that does add a request. The header is set before the rules request finishes, so a
|
|
170
|
+
guard booting with no cached rules declares `no-managed-rules` and then receives managed rules on that
|
|
171
|
+
same response. When that happens it sends **one immediate POST to the detections endpoint containing the
|
|
172
|
+
corrected state and no detections at all** — an empty `detections` array plus `reporting_state`. It is
|
|
173
|
+
sent once per process, only when the state changed, and never when the guard already had cached rules.
|
|
174
|
+
Without it, a guard with rule refreshing switched off would leave Patchstack holding the pre-resolution
|
|
175
|
+
answer for the life of the process.
|
|
176
|
+
|
|
177
|
+
What a detection report contains, per matched rule — on every phase, whatever fired it: the rule id, the revision of the rule when the
|
|
178
|
+
bundle carried one, the identifier of the rule bundle in use, which phase matched,
|
|
179
|
+
whether it was enforced, the request path **with the query string's values removed**,
|
|
180
|
+
that query's parameter names, the method, and a timestamp. Each batch also carries a count of reports dropped when traffic outran the flush,
|
|
181
|
+
so a partial sample is not read as a complete one.
|
|
182
|
+
|
|
183
|
+
Two fields depend on the phase, because one kind of detection has a client and the other does not. A
|
|
184
|
+
**request or response** detection also carries the user agent,
|
|
185
|
+
and the client address together with where that address came from.
|
|
186
|
+
An **egress** detection — a rule that fired on a call your application made outbound — carries neither:
|
|
187
|
+
the call was your application's own, so there is no visitor to attribute it to, and those fields read
|
|
188
|
+
`null` and `unavailable` rather than being guessed at. "What values a report can contain" below says the
|
|
189
|
+
same thing about captured evidence.
|
|
190
|
+
|
|
191
|
+
Every field is bounded in size, and an event that had to be shortened says so.
|
|
192
|
+
`truncated` lists **which fields were shortened**.
|
|
193
|
+
`parameters_total` records **how many parameters the rule reads**, when a rule reads more than the event
|
|
194
|
+
names.
|
|
195
|
+
`query_keys_total` records **how many query parameters the request carried**, counted as DISTINCT names,
|
|
196
|
+
when it carried more than the event lists — a parameter repeated three times is one name to look up. The
|
|
197
|
+
names themselves are the ones the guard addresses a parameter by, so `?first+name=x` is reported as
|
|
198
|
+
`first name`. Both appear only when something really was shortened, so their absence is not a claim of its own —
|
|
199
|
+
and a shortened route or rule id is marked rather than passed off as complete, because a reader must not
|
|
200
|
+
use one as a key believing it names the whole thing.
|
|
201
|
+
|
|
202
|
+
Delivery is retried, up to four attempts per batch, with exponential backoff and jitter, honouring a
|
|
203
|
+
`Retry-After` header when the endpoint sets one. Only failures worth retrying are retried — unreachable,
|
|
204
|
+
rate-limited, or a server error; a batch that was refused on its merits is not sent again. Every attempt
|
|
205
|
+
of one batch carries the same `Idempotency-Key` header, and a different batch carries a different one, so
|
|
206
|
+
a redelivery is identifiable as the same batch rather than a new one — an acknowledgement can be lost
|
|
207
|
+
after the server has already taken a batch. One request is in flight at a time, so a slow endpoint slows
|
|
208
|
+
the queue rather than opening more sockets; each attempt is abandoned after 10 seconds, so a request that
|
|
209
|
+
never settles cannot hold that slot; and a batch that exhausts its attempts is dropped and counted rather
|
|
210
|
+
than retried forever. Stopping a guard makes one last attempt at whatever is outstanding and counts
|
|
211
|
+
anything it could not send. `stop()` returns a promise that settles once the reporters have finished or
|
|
212
|
+
been given up on, so a shutdown handler can `await protection.stop()` instead of racing the last batch
|
|
213
|
+
against process exit. Each reporter has its own budget, and when it runs out that reporter is ended: its
|
|
214
|
+
requests are aborted, it starts nothing further, and it discards what it was holding. An abort is a
|
|
215
|
+
request to stop, not a guarantee — a transport that ignores it is detached rather than completed, so
|
|
216
|
+
"resolved" means the reporter is finished with it, and a runtime that kills the process still wins
|
|
217
|
+
regardless. Every detection event ends up delivered, refused or dropped and is reported in the health
|
|
218
|
+
counts; block-log records have no counters, so one lost to a failed send or an expired shutdown is
|
|
219
|
+
reported nowhere.
|
|
220
|
+
|
|
221
|
+
The client address is reported with its **provenance**, because an address is only as trustworthy as
|
|
222
|
+
whatever supplied it. `client_ip_source` is one of `runtime` (the address the transport observed),
|
|
223
|
+
`trusted-proxy` (read from a forwarded header, through peers you declared via `trustedProxy`), or
|
|
224
|
+
`unavailable`. When it is `unavailable` the `client_ip` field is **omitted entirely** rather than sent
|
|
225
|
+
empty, so a missing address cannot read as a failed lookup of a real one. A forwarded header is never
|
|
226
|
+
trusted implicitly: with no `trustedProxy` policy the address is whatever the transport observed, and in a
|
|
227
|
+
runtime that exposes no transport peer there is no address to report at all.
|
|
228
|
+
|
|
229
|
+
### Behaviour change: how the client address is determined
|
|
230
|
+
|
|
231
|
+
The guard resolves the client address itself, once per request, and shares that one answer with rule
|
|
232
|
+
matching, block logging and detection reports — so those cannot disagree about who a request came from.
|
|
233
|
+
Two consequences if you are upgrading:
|
|
234
|
+
|
|
235
|
+
- **Express and Node: forwarded headers are no longer read implicitly.** Earlier versions took the
|
|
236
|
+
address from `X-Forwarded-For`, `CF-Connecting-IP` or `X-Real-IP` (the Node guard), or from `req.ip`
|
|
237
|
+
(the Express guard, where it reflects Express's own `trust proxy` setting). Neither source can be
|
|
238
|
+
verified by the guard, and any client can send those headers, so both guards now read the transport
|
|
239
|
+
peer. **If your app runs behind a proxy or load balancer, addresses will now show as the proxy's**
|
|
240
|
+
until you declare your proxies with `trustedProxy` (below) — which affects attribution in reports and
|
|
241
|
+
any rule matching on `server.ip` or `REMOTE_ADDR`.
|
|
242
|
+
- **Fetch runtimes report no address at all.** A WHATWG `Request` exposes no transport peer, so a Fetch
|
|
243
|
+
guard (Workers, Deno, Bun, edge) has nothing to observe, and no forwarded header is accepted in its
|
|
244
|
+
place under any `trustedProxy` policy: `client_ip_source` is `unavailable` and no address is sent.
|
|
245
|
+
Earlier versions reported the forwarded header here, so an address-scoped rule that appeared to work on
|
|
246
|
+
such a runtime was matching a client-supplied value.
|
|
247
|
+
|
|
248
|
+
`trustedProxy` is the only way to make a forwarded header count. It takes the proxies you actually run —
|
|
249
|
+
`{ peers: ['10.0.0.0/8'] }`, or `{ hops: 1 }` to trust that many hops in from the peer, plus optional
|
|
250
|
+
`header` and `isTrusted` — and the chain is then read from your application inward, stopping at the first
|
|
251
|
+
hop you have not declared. There are no built-in provider presets: a header a provider sets is
|
|
252
|
+
indistinguishable from one a client sent unless you say which peers may set it.
|
|
159
253
|
|
|
160
254
|
The parameter names are **identifiers, and they name the request region they refer to** — `post.title`,
|
|
161
255
|
`get.redirect_to`, `cookie.session`, `server.HTTP_AUTHORIZATION`. So a rule that inspects a cookie or an
|
|
@@ -163,16 +257,79 @@ The parameter names are **identifiers, and they name the request region they ref
|
|
|
163
257
|
definition, not from your traffic, so they describe what is being screened rather than what any request
|
|
164
258
|
contained.
|
|
165
259
|
|
|
166
|
-
What
|
|
167
|
-
|
|
168
|
-
|
|
260
|
+
### What values a report can contain
|
|
261
|
+
|
|
262
|
+
**A request or response detection** carries the request's method and path,
|
|
263
|
+
the query string's parameter **names**,
|
|
264
|
+
the user agent, the client address with its provenance, and a timestamp.
|
|
265
|
+
|
|
266
|
+
**An egress detection** — a rule that fired on a request your application made outbound — carries the
|
|
267
|
+
outbound method and path, the query's parameter names, and a timestamp. It carries **no user agent and no
|
|
268
|
+
client address**: the call was your application's own, so there is no visitor to attribute it to, and
|
|
269
|
+
those fields read `null` and `unavailable` rather than being guessed at.
|
|
270
|
+
|
|
271
|
+
Beyond that baseline, either can include the **values of the parameters a rule names** — and nothing
|
|
272
|
+
else. Counting that a
|
|
273
|
+
rule fired is not enough to act on it: whoever triages a detection still has to decide whether the request
|
|
274
|
+
was really an attack, and for that they need to see what the rule saw.
|
|
275
|
+
|
|
276
|
+
**A rule earns each permission by naming what it reads.** What may be captured is derived from the rule
|
|
277
|
+
itself, never configured per site:
|
|
278
|
+
|
|
279
|
+
- a rule naming a parameter (`post.title`, `cookie.session`, `server.HTTP_AUTHORIZATION`) permits **that
|
|
280
|
+
parameter's value**, because the rule was written to inspect it;
|
|
281
|
+
- a prefix (`post.field_*`) permits the values of keys that match, and no others;
|
|
282
|
+
- a rule reading `raw` or `all` — the whole request — permits **nothing at all**, so the broadest rules
|
|
283
|
+
grant the narrowest capture;
|
|
284
|
+
- **response** values are never captured: the phase that reads them exists to redact secrets, and
|
|
285
|
+
capturing them would collect the very values that redaction stops leaving;
|
|
286
|
+
- **raw request bytes** need an explicit, reviewed opt-in on the individual rule, and are then limited to
|
|
287
|
+
a short prefix of the body.
|
|
288
|
+
|
|
289
|
+
**Everything is bounded, and the bounds report themselves.**
|
|
290
|
+
At most 10 values per detection, at most 512 characters each, and at most 5 values from any one prefix. A value shortened to fit is marked; values a bound
|
|
291
|
+
left out are counted; a value refused because it was not a plain string, number or boolean is counted
|
|
292
|
+
separately; and a read that failed is counted as a failure rather than as absence — so a short list is
|
|
293
|
+
never mistaken for a complete one.
|
|
294
|
+
|
|
295
|
+
**A `capture.plan` identifies the permissions, not the rule.** Each report carries a `capture.plan` reference derived
|
|
296
|
+
from the permissions themselves — which parameters, which prefixes, which bounds — so what a given report
|
|
297
|
+
was permitted to include can be established after the fact, without the rule in front of you. It
|
|
298
|
+
identifies the PERMISSIONS, not the rule: two different rules that read the same parameters share one
|
|
299
|
+
reference, and the rule document is identified by `rule_id` with `rule_revision`.
|
|
300
|
+
|
|
301
|
+
**Capture is the union of everything the rule reads, not only the condition that matched.** The engine
|
|
302
|
+
reports which rule fired, not which of its conditions did, so a rule reading `post.title` and
|
|
303
|
+
`cookie.session` permits both values whichever one triggered the detection. A rule scoped to one parameter
|
|
304
|
+
captures one; a broad rule captures what it is broad about.
|
|
305
|
+
|
|
306
|
+
**One header value always travels, whatever the rule names: the User-Agent** — on a request or response
|
|
307
|
+
detection, where there is a client to attribute. It is part of the baseline above, because attribution is
|
|
308
|
+
what this channel is for and a detection without it cannot be told from another client's. It is the only
|
|
309
|
+
exception to the rule-scoped policy, and the only header value sent without a rule naming it.
|
|
310
|
+
|
|
311
|
+
**What a report never contains:** the value of any parameter the matched rule does not name — the
|
|
312
|
+
User-Agent above excepted; any response body, header or status value;
|
|
313
|
+
the request body, other than the reviewed raw prefix above;
|
|
314
|
+
and anything at all from a rule that reads the whole request without that opt-in.
|
|
315
|
+
|
|
316
|
+
**One qualification on the query string.** The exclusion above is about baseline URL metadata: `route` and
|
|
317
|
+
`query_keys` describe a URL without disclosing what was in it. It is not a promise about captured
|
|
318
|
+
evidence. A rule that names `egress.url` reads the outbound URL, so its capture carries that URL as the
|
|
319
|
+
rule read it — query values included. That is the rule-scoped policy working as described, not an
|
|
320
|
+
exception to it: the rule named the parameter, so the parameter's value travels. The value recorded is the request as the
|
|
321
|
+
engine resolved it — URL- and entity-decoded — and not the result of a rule's own further mutations.
|
|
322
|
+
|
|
323
|
+
Reports are batched, capped in memory, retried a bounded number of times, and dropped if Patchstack cannot
|
|
169
324
|
be reached — a reporting failure never delays or fails a request.
|
|
170
325
|
|
|
171
|
-
The endpoint needs a credential, so
|
|
172
|
-
|
|
326
|
+
The endpoint needs a credential, so an enrolled site with none resolved starts nothing: the guard warns
|
|
327
|
+
once at boot and `protection.detectionReporting` reads `unavailable-no-credential` instead of `on`.
|
|
173
328
|
When reporting is on, `protection.detectionHealth()` returns local counts — detections attempted,
|
|
174
329
|
acknowledged, refused or unreachable, dropped for queue pressure — and the time of the last
|
|
175
|
-
acknowledgement.
|
|
330
|
+
acknowledgement. Counts for the state POST described above are kept separately under `capability`, since it
|
|
331
|
+
carries no detections and would otherwise read as one. Those counts stay in your process; nothing extra is
|
|
332
|
+
sent to report them.
|
|
176
333
|
|
|
177
334
|
`protection.stop()` stops everything the guard has running in the background — the rule-refresh loop, the
|
|
178
335
|
block-log reporter, the detection reporter — and flushes what is buffered. `protection.stopRefresh()` is
|
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.com) for continuous vulnerability monitoring. Scans your `package-lock.json` and reports installed packages so Patchstack can match them against its vulnerability database and notify you when something needs patching.
|
|
4
4
|
|
|
5
|
-
For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patchstack-website`, `patchstack-connect`), see [`patchstack/saas` → `docs/ecosystem.md`](https://github.com/patchstack/saas/blob/main/docs/ecosystem.md).
|
|
6
|
-
|
|
7
5
|
## Agent-assisted setup
|
|
8
6
|
|
|
9
7
|
Copy this request into a coding assistant, or run the same command yourself:
|
|
@@ -89,6 +87,7 @@ patchstack-connect demo-guide node-serialize Read-only, state-aware instru
|
|
|
89
87
|
local demo, including the next exact command,
|
|
90
88
|
expected proof, and cleanup.
|
|
91
89
|
patchstack-connect help Print help
|
|
90
|
+
patchstack-connect --version Print the installed version
|
|
92
91
|
|
|
93
92
|
Options (for scan, setup, and status):
|
|
94
93
|
--site-uuid <uuid> Override the configured site UUID
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pulse-token.ts"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
5
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
6
|
|
|
6
7
|
// src/parsers/index.ts
|
|
7
8
|
import { access } from "fs/promises";
|
|
@@ -2083,38 +2084,83 @@ var PROD_MARKER_GLOBAL = "__PATCHSTACK_PROD__";
|
|
|
2083
2084
|
var REGION_OPEN = "{/* #region patchstack (managed by patchstack-connect \u2014 do not edit) */}";
|
|
2084
2085
|
var REGION_CLOSE = "{/* #endregion patchstack */}";
|
|
2085
2086
|
var REGION_RE = /[ \t]*\{\/\* #region patchstack[\s\S]*?#endregion patchstack \*\/\}\n?/g;
|
|
2087
|
+
function findJsxShellAnchor(source, tagName) {
|
|
2088
|
+
const candidates = new RegExp(`^([ \\t]*)<${tagName}(?=[\\s/>])`, "gm");
|
|
2089
|
+
let candidate;
|
|
2090
|
+
while ((candidate = candidates.exec(source)) !== null) {
|
|
2091
|
+
let quote = null;
|
|
2092
|
+
let escaped = false;
|
|
2093
|
+
let braces = 0;
|
|
2094
|
+
let blockComment = false;
|
|
2095
|
+
let lineComment = false;
|
|
2096
|
+
for (let index = candidates.lastIndex; index < source.length; index += 1) {
|
|
2097
|
+
const char = source[index];
|
|
2098
|
+
const next = source[index + 1];
|
|
2099
|
+
if (lineComment) {
|
|
2100
|
+
if (char === "\n") lineComment = false;
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (blockComment) {
|
|
2104
|
+
if (char === "*" && next === "/") {
|
|
2105
|
+
blockComment = false;
|
|
2106
|
+
index += 1;
|
|
2107
|
+
}
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
if (quote !== null) {
|
|
2111
|
+
if (escaped) {
|
|
2112
|
+
escaped = false;
|
|
2113
|
+
} else if (char === "\\") {
|
|
2114
|
+
escaped = true;
|
|
2115
|
+
} else if (char === quote) {
|
|
2116
|
+
quote = null;
|
|
2117
|
+
}
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2121
|
+
quote = char;
|
|
2122
|
+
} else if (braces > 0 && char === "/" && next === "*") {
|
|
2123
|
+
blockComment = true;
|
|
2124
|
+
index += 1;
|
|
2125
|
+
} else if (braces > 0 && char === "/" && next === "/") {
|
|
2126
|
+
lineComment = true;
|
|
2127
|
+
index += 1;
|
|
2128
|
+
} else if (char === "{") {
|
|
2129
|
+
braces += 1;
|
|
2130
|
+
} else if (char === "}") {
|
|
2131
|
+
if (braces === 0) break;
|
|
2132
|
+
braces -= 1;
|
|
2133
|
+
} else if (char === ">" && braces === 0) {
|
|
2134
|
+
const openingTag = source.slice(candidate.index, index + 1);
|
|
2135
|
+
if (/\/\s*>$/.test(openingTag)) break;
|
|
2136
|
+
return { end: index + 1, indent: candidate[1] ?? "" };
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return null;
|
|
2141
|
+
}
|
|
2086
2142
|
function ensureMarkerInJsxShell(source, framework) {
|
|
2087
2143
|
if (!hasJsxShell(framework)) {
|
|
2088
2144
|
return { source, action: "unsupported" };
|
|
2089
2145
|
}
|
|
2090
2146
|
const stripped = source.replace(REGION_RE, "");
|
|
2091
2147
|
if (stripped.includes(PROD_MARKER_GLOBAL)) {
|
|
2092
|
-
return { source, action: "manual" };
|
|
2148
|
+
return { source: stripped, action: "manual" };
|
|
2093
2149
|
}
|
|
2094
2150
|
const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2151
|
+
for (const tagName of ["head", "body"]) {
|
|
2152
|
+
const anchor = findJsxShellAnchor(stripped, tagName);
|
|
2153
|
+
if (anchor === null) continue;
|
|
2154
|
+
const indent = `${anchor.indent} `;
|
|
2155
|
+
const remainder = stripped.slice(anchor.end);
|
|
2156
|
+
const separator = remainder.startsWith("\n") || remainder.startsWith("\r") ? "" : "\n";
|
|
2098
2157
|
return {
|
|
2099
|
-
source: `${stripped.slice(0,
|
|
2100
|
-
${
|
|
2158
|
+
source: `${stripped.slice(0, anchor.end)}
|
|
2159
|
+
${block(indent)}${separator}${remainder}`,
|
|
2101
2160
|
action: "added"
|
|
2102
2161
|
};
|
|
2103
2162
|
}
|
|
2104
|
-
|
|
2105
|
-
const match = stripped.match(anchor);
|
|
2106
|
-
if (match?.index === void 0) {
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
const end = match.index + match[0].length;
|
|
2110
|
-
const indent = `${match[1] ?? ""} `;
|
|
2111
|
-
return {
|
|
2112
|
-
source: `${stripped.slice(0, end)}
|
|
2113
|
-
${block(indent)}${stripped.slice(end)}`,
|
|
2114
|
-
action: "added"
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
|
-
return { source, action: "no-anchor" };
|
|
2163
|
+
return { source: stripped, action: "no-anchor" };
|
|
2118
2164
|
}
|
|
2119
2165
|
function ensureSourceMarker(cwd, shell, framework) {
|
|
2120
2166
|
if (shell === null) {
|
|
@@ -2123,7 +2169,7 @@ function ensureSourceMarker(cwd, shell, framework) {
|
|
|
2123
2169
|
const file = path7.resolve(cwd, shell);
|
|
2124
2170
|
const before = readFileSync(file, "utf8");
|
|
2125
2171
|
const result = ensureMarkerInJsxShell(before, framework);
|
|
2126
|
-
if (result.source !== before
|
|
2172
|
+
if (result.source !== before) {
|
|
2127
2173
|
writeFileSync(file, result.source);
|
|
2128
2174
|
}
|
|
2129
2175
|
return { ...result, shell };
|
|
@@ -4912,12 +4958,16 @@ var FS_PACKAGES = ["fs-extra", "graceful-fs", "memfs"];
|
|
|
4912
4958
|
var isFsPackage = (pkg) => /^node:fs(\/promises)?$/.test(pkg) || FS_PACKAGES.includes(pkg);
|
|
4913
4959
|
var EXEC_PACKAGES = ["execa", "cross-spawn", "shelljs", "zx"];
|
|
4914
4960
|
var isExecPackage = (pkg) => pkg === "node:child_process" || EXEC_PACKAGES.includes(pkg);
|
|
4961
|
+
var DESERIALIZE_CALLS = /^unserialize$/;
|
|
4962
|
+
var DESERIALIZE_PACKAGES = ["node-serialize"];
|
|
4963
|
+
var isDeserializePackage = (pkg) => DESERIALIZE_PACKAGES.includes(pkg);
|
|
4915
4964
|
function recognizedSinkKinds(pkg) {
|
|
4916
4965
|
const kinds = [];
|
|
4917
4966
|
if (isDbPackage(pkg)) kinds.push("db");
|
|
4918
4967
|
if (isFsPackage(pkg)) kinds.push("fs");
|
|
4919
4968
|
if (isExecPackage(pkg)) kinds.push("exec");
|
|
4920
4969
|
if (isHttpPackage(pkg)) kinds.push("http");
|
|
4970
|
+
if (isDeserializePackage(pkg)) kinds.push("eval");
|
|
4921
4971
|
return kinds;
|
|
4922
4972
|
}
|
|
4923
4973
|
function collectLocalSinks(sf, ts, bindings, ctx) {
|
|
@@ -5031,6 +5081,9 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
5031
5081
|
if (EXEC_CALLS.test(method) && b.pkg && isExecPackage(b.pkg)) {
|
|
5032
5082
|
push({ kind: "exec", package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
|
|
5033
5083
|
}
|
|
5084
|
+
if (DESERIALIZE_CALLS.test(method) && b.pkg && isDeserializePackage(b.pkg)) {
|
|
5085
|
+
push({ kind: "eval", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
|
|
5086
|
+
}
|
|
5034
5087
|
if (HTTP_MEMBER_METHODS.has(method)) {
|
|
5035
5088
|
if (b.pkg && isHttpPackage(b.pkg)) {
|
|
5036
5089
|
push({ kind: "http", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
|
|
@@ -5058,6 +5111,9 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
5058
5111
|
push({ kind: "exec", package: pkg, op: name, attribution: "import", ...spanOf(n) });
|
|
5059
5112
|
}
|
|
5060
5113
|
if (name === "eval" && trueGlobal) push({ kind: "eval", op: "eval", attribution: "global", ...spanOf(n) });
|
|
5114
|
+
if (DESERIALIZE_CALLS.test(name) && pkg && isDeserializePackage(pkg)) {
|
|
5115
|
+
push({ kind: "eval", package: pkg, op: name, attribution: "import", ...spanOf(n) });
|
|
5116
|
+
}
|
|
5061
5117
|
}
|
|
5062
5118
|
}
|
|
5063
5119
|
if (ts.isNewExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "Function" && !bindings.locals.has("Function") && !isShadowedByEnclosingBinding(n, "Function", ts)) {
|
|
@@ -5134,6 +5190,7 @@ var CANDIDATE_FAMILIES = {
|
|
|
5134
5190
|
};
|
|
5135
5191
|
function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
5136
5192
|
if (sinkKind === "eval" && method === "Function") return index === total - 1 ? "code" : "args";
|
|
5193
|
+
if (sinkKind === "eval" && method === "unserialize") return index === 0 ? "code" : "unknown";
|
|
5137
5194
|
const table = method ? ARGUMENT_ROLES[sinkKind]?.[method] : void 0;
|
|
5138
5195
|
return table?.[index] ?? "unknown";
|
|
5139
5196
|
}
|
|
@@ -6751,6 +6808,10 @@ Usage:
|
|
|
6751
6808
|
the old one must be updated. Not usable in CI
|
|
6752
6809
|
patchstack-connect help Print this message
|
|
6753
6810
|
|
|
6811
|
+
Global:
|
|
6812
|
+
--version Print the installed version of this package and exit
|
|
6813
|
+
--help Print this help and exit
|
|
6814
|
+
|
|
6754
6815
|
Options (for scan, setup, status, and uninstall):
|
|
6755
6816
|
--site-uuid <uuid> Override the configured site UUID
|
|
6756
6817
|
--endpoint <url> Override the API endpoint
|
|
@@ -7445,8 +7506,21 @@ async function runMarkBuild(args) {
|
|
|
7445
7506
|
);
|
|
7446
7507
|
return 0;
|
|
7447
7508
|
}
|
|
7509
|
+
function packageVersion() {
|
|
7510
|
+
try {
|
|
7511
|
+
const require2 = createRequire2(import.meta.url);
|
|
7512
|
+
const manifest = require2("../package.json");
|
|
7513
|
+
return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : "unknown";
|
|
7514
|
+
} catch {
|
|
7515
|
+
return "unknown";
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7448
7518
|
async function main() {
|
|
7449
7519
|
const args = parseArgs(process.argv);
|
|
7520
|
+
if (args.flags.has("version") || args.command === "version") {
|
|
7521
|
+
console.log(packageVersion());
|
|
7522
|
+
return 0;
|
|
7523
|
+
}
|
|
7450
7524
|
if (args.flags.has("help") || args.command === "help" || args.command === null) {
|
|
7451
7525
|
console.log(HELP);
|
|
7452
7526
|
return 0;
|