Commit Graph
28 Commits
Author SHA1 Message Date
morluto 2f4c588584 fix(artifacts): require each reader traversal to declare its coverage (#10572)
* fix(artifacts): require each reader traversal to declare its coverage

* docs(artifacts): state that silent traversals report unknown coverage
2026-11-15 04:07:21 +08:00
7aca4cef64 feat(audit): report what a shipped app exposes to static analysis (#8525)
* feat(audit): report what a shipped app exposes to static analysis

Adds `audit_release_exposure` and `rea audit-release-exposure` to report
source maps, Electron fuse implications, extractable payloads, debug
information, build metadata, and credential markers. Checks carry stable
IDs, observations, statuses, and fixed remediation, while incomplete and
unassessed checks remain visible without implying a security verdict.
Credential findings include only rule IDs and locations. The CLI
supports `--fail_on` severity thresholds and exits 3 when an exposed
check meets the threshold.

* fix(audit): keep incomplete coverage unknown across text, fuses, Go flags and cancellation

Probe files with unrecognized names as text, keep undecided fuse slices
unknown, forward opaque container coverage, read paths from Go build flags,
and compare inventoried digests before accepting owner facts.

Keep typed cleanup failures and attach the partial report when a later owner
is cancelled. Pin the verifier's @electron/fuses version and move the tests
to the named contract family.

* fix(audit): match package members by source spelling and close remaining coverage gaps

Reopen package members by their source spelling and keep caller path
traversal in directory reads. Parse quoted and attached Go build flags, bind a
native image fuse read to its own digest, and keep reader resources and the
audit's partial report when cleanup or cancellation combine failures.

* fix(release-exposure): bind package members by ordinal and follow named map references

Package members were matched to container entries by sourcePath, which is
not unique in a PyInstaller executable, through a queue that only moved when
a member was reopened. Bind by container-relative path and duplicate ordinal,
the numbering inventory uses, so read order cannot change which bytes a
member gets.

A sourceMappingURL naming a file without a map extension was ignored. Local
targets are now examined as maps whatever their name, and a target already
scanned as plain text leaves coverage unknown.

* fix(release-exposure): match data: source map scheme case-insensitively

URL schemes are case-insensitive, so an inline declaration such as
//# sourceMappingURL=DATA:application/json;base64,... was routed to the
local-reference path, which ignores scheme URLs. Its embedded sources were
silently dropped and source_map_sources reported not_observed.

* fix(release-exposure): normalize declared map targets and count unreached ones

A sourceMappingURL that decodes to a decomposed name (cafe%CC%81.json)
never matched the inventoried member, whose logical path is NFC, so the
map was scanned as ordinary text and its sourcesContent went uncounted.
Map-target lookups now use an NFC key on both the record and lookup
side; the declared URL is left as written.

A declared map target that is never reached is now counted as a skipped
map, so the source-map checks read as unknown coverage instead of
not_observed.

* fix(release-exposure): recover indexed leaves after any map format failure

An indexed map whose one leaf has malformed mappings (such as AA) threw a
format failure that only recovered leaves for unsupported sections, so a
valid sibling leaf with sourcesContent was dropped and
javascript.source_map_sources read unknown instead of exposed. Leaves are
now recovered for any format failure, with the envelope parse guarded so
invalid JSON still falls through to the unparsed count.

A nested package that inventory recognized but did not expand was probed
as a plain binary and left the source-map checks at not_observed. It now
marks coverage unknown and records a limitation.

Reader tests assert the javascript.source_map_sources status for the NFC
map-name cases as well.

* fix(release-exposure): count an unexpanded nested package as a readable member

An unencrypted nested package that inventory recognized but did not
expand is still a member its parent package already read. Count it as
readable so packaging.extractable_payload reports exposed, while its
own contents keep coverage unknown.

* fix(release-exposure): bound the indexed leaf fallback by decoder limits

The fallback walk after a map format failure expanded every section and
built a full ancestor path per leaf, so a map refused for nesting depth
could still allocate far past the decoder budgets. Walk only to the
64-level section depth and the shared record budget, keeping leaf maps
without materializing section paths.

* fix(release-exposure): collect UNC roots from Go flag paths

A flag such as -I\\buildhost\sdk\include matched none of the path roots
in GO_FLAG_PATH, so a network path that was the only one in complete
build info left go.build_paths at not_observed. Accept \\host\share
roots alongside the slash-rooted, dot-relative and drive-letter forms.

* fix(release-exposure): keep Electron undecided without positive proof

The desktop owner names Electron only from an ASAR layout, so an asar: false
build has no candidate. A complete scan with no candidate was reported as
not_electron, which outranks unresolved and safe fuse outcomes and hid them
as not_applicable.

Leave the family unknown unless the fuse holder rules Electron out: an app
bundle with no Electron Framework, or a directory with complete coverage
whose native images were all read and hold no sentinel. A directory with no
native image is not proof, so its fuse checks now report unknown.

* fix(release-exposure): decode wide text members before scanning

A member's kind bypassed the NUL guard, so a UTF-16 plist was decoded as
UTF-8, matched nothing and still counted as scanned. A directory holding
only that member reported text.credential_markers as not_observed.

Decode by byte order mark at one boundary instead. UTF-16 is scanned in
either byte order, and inline maps read the same text. UTF-32 and NULs no
mark explains count as skipped, whatever the member's kind. A binary
plist keeps its raw scan. Unnamed members with a mark now reach the
scanner, and the plist decoder shares the same mark detection.

* fix(release-exposure): follow file: URL source map declarations

The scheme guard in noteLocalMapReference returned on every URL scheme, so
a declaration such as file:///release/debug.json was dropped with the remote
ones. A member without a .map name was then scanned as ordinary text and
javascript.source_map_sources reported not_observed.

Convert file: URLs with fileURLToPath and record them in mapTargets through
the same step as relative declarations. A target that is never reached, or
that was already scanned as text, counts as skipped, and so does a malformed
file: URL. A file: URL names one absolute location, so a .map target outside
the inventory stays unassessed rather than being trusted by its name. Remote
schemes stay ignored.

* fix(release-exposure): match backslash-relative Go flag paths

GO_FLAG_PATH accepted dot-relative roots only with a forward slash, so
`-I..\sdk\include` and `-I.\sdk` produced no path and go.build_paths came
back not_observed. Accept either separator after the dots.

The CLI acceptance test now expects unknown for electron.run_as_node on a
directory with no native images, matching the service.

* fix(release-exposure): keep fuses unknown on incomplete inventory

A directory inventory that stops at an acquisition limit never reports
the images it did not reach, so the native image sample has no omission
and safe fuses on an included helper settled to not_observed even though
scan coverage was unknown.

Treat unknown coverage like a truncated sample until the Electron
executable is accounted for. A selected native file is that executable,
so it keeps its decided fuse result.

* fix(release-exposure): decode binary plists before scanning

A binary plist stores each string as ASCII or UTF-16BE. Scanning the raw
bytes as UTF-8 missed any string the encoder kept in UTF-16, such as one
with a non-ASCII character, and still counted the member as fully
scanned, so text.credential_markers read not_observed.

Decode through the plist owner and scan its strings, dictionary keys and
data. A plist that cannot be decoded keeps its raw bytes in the scan and
counts as skipped, so coverage stays unknown.

* fix(release-exposure): keep UTF-8 members that hold a NUL

Any NUL in a member's first 512 bytes made the decoder give up, so a script
with a literal NUL in a string never reached the credential or inline-map
checks. An exposed marker in it read as unknown, and --fail_on high exited 0.

Validity alone cannot tell wide text from UTF-8, since ASCII in UTF-16 is
valid UTF-8. Treat a header that is an eighth or more NUL bytes as wide text
and anything not UTF-8 as undecoded, and scan the rest.

The docs now separate BOM-marked UTF-16, which is scanned, from wide text
with no byte order mark, which is not.

* fix(audit): scan every string a binary plist stores

The plist scan read only the decoded value, which drops objects the root no
longer references and entries a later duplicate key replaced. Those bytes
still ship, but the audit marked the scan complete, so a credential stored
there read as not observed and --fail_on high exited 0.

Read strings, keys and data from the object table instead. A stored object
that cannot be read leaves the member unassessed, and a total byte budget
bounds table entries that overlap to repeat the same text.

* fix(audit): keep packaged native images from ruling out Electron

The applicability check only looked at the "native images" omission. An
Electron executable inside a compressed .NET or PyInstaller member is recorded
under "packaged native images", so a directory whose on-disk hosts had no fuse
sentinel read as not Electron and all four fuse checks as not applicable.

Treat that omission as an unread image too.

* fix(audit): keep readable plist text when one object is unreadable

Throwing on one unreadable stored object discarded every string already
decoded, so a UTF-16BE credential beside a truncated unused object was lost:
the raw bytes that replaced it cannot match NUL-separated text.

Return the decoded text with complete false instead, and keep the raw bytes
in the scan beside it so ASCII in the unread object is still seen. The member
stays unassessed. Text past the budget ends the same way.

* fix(audit): read escaped inline maps and extensionless SEA entries

A data URL may percent-escape its base64 payload, so a map such as one padded
with %3D did not decode and the audit counted no embedded source map. Decode
the escapes before reading the base64.

A directory walk classed a SEA entry by its code path, so a main script whose
code path names no script was never scanned as JavaScript and its inline map
went unseen. Class it by the role the SEA metadata gives it.

* fix(audit): check plist text against the budget before decoding it

decodeBinaryPlistText decoded each stored object before comparing its size
with the remaining budget, and it keeps going after an object is rejected so
later text is still read. A plist under the input limit could repeat a large
range through overlapping table entries and force gigabytes of decoding that
the audit then discarded.

Separate finding a stored payload from decoding it, so a range past the
remaining budget is rejected without allocating its text. Earlier text and
the incomplete result stay as they were.

* fix(audit): count a packaged PE with an unread header as a native image

A package member whose PE signature lies beyond the 4 KiB probe recorded only
a PDB failure. The image was neither read nor listed under packaged native
images, so a directory whose on-disk hosts had no fuse sentinel read as not
Electron and all four fuse checks as not applicable.

Count it as a packaged native image, the omission that keeps the question
open, and keep probing it for text as before.

* fix(audit): keep cleanup-incomplete errors from composed readers

A desktop, fuse, Go or Rust failure became a string on that check, so an
error with unfinished cleanup lost its resources and live bindings while the
audit reported success, and the MCP handler does not close the service after
each request.

Treat unfinished cleanup like cancellation: stop the remaining owners and
return the error with the partial audit. When the owner attached its own
partial, keep the error and replace the partial, so cleanup stays attributed.

* fix(audit): keep safe fuses unknown past a packaged native image

The fuse check counted sampled-out images as possibly unread but not native
images inside a package. A complete directory scan with a safe-fuse image on
disk, no ASAR hint, and an unopened packaged image reported the four Electron
checks as not_observed, although the Electron executable could be that image.

The fuse check, the Go and Rust checks, and the Electron rule-out each decided
this for themselves and had drifted apart. Move the decision to one domain
predicate, nativeImagesMayBeUnread, and use it in all three. A hinted
executable that was read still settles the check.

* fix(audit): settle fuses only on an identified host, and read Bun and HTML maps

A retained helper set electron_executable_included because the rank marks
every .exe or extensionless sibling of resources/app.asar. With more than four
siblings, or an inventory that stopped early, safe fuses on a helper reported
not_observed while the host went unread. Rank stays a sampling hint. The check
now settles only on an executable named as the host, or when inventory was
complete and every executable that could be the host was sampled.

A Bun sourcemap region is named .bunmap, so its role was lost and a complete
audit reported no source maps. Carry the region role from the reader's metadata
into the member view, in the expanded and unexpanded paths, and treat it as a
JavaScript map so an unparsable region stays unresolved, not a debug companion.

An HTML member was scanned as text but its inline scripts were never checked
for source maps. The HTML owner now returns each body, the reader examines it
like a script file, and bodies past the owner's limit count as skipped.

* fix(audit): charge map reads to the count before opening the member

An oversized or unreadable .map returned before readSourceMap counted it, so
the allowance of 32 never ran out. An unexpanded .NET or PyInstaller package
could then decompress every listed map member, about 1 MiB each, up to the
4,096 entry cap.

The count bounds reads, so examineMap now reserves the slot before it opens the
member. A map past the count is skipped unopened, and a linker map past it is
still settled from its bounded prefix. An inline data: map is already in memory
and keeps its charge at the point it is parsed.

* fix(audit): examine scripts HTML loads whatever they are called

isJavaScriptMember knows a script by its suffix or an assigned kind, and the
reader dropped the script references the HTML owner parses. An extensionless
main loaded by <script src="main"> was scanned as ordinary text, so a complete
inventory reported javascript.source_map_sources not_observed with shipped
sources in its inline map.

The reader now resolves each JavaScript script reference against the page and
its base href, and a local target without a JavaScript name is examined as a
script when reached. A target read before its HTML file, never reached, or not
placeable on disk counts as a skipped map, so the check stays unknown.

* fix(audit): charge sibling map reads to the map count

Every generic .map beside a selected file got a full 1 MiB read and a
parse, with no cumulative bound, so the 4,096-entry listing allowed about
4 GiB of reading for one small selection.

Move the slot rule into chargeMapRead, which examineMap and sibling
classification now share. Past the count a sibling reads only the header:
a linker map is still identified, and one that may be a source map stays
a skipped companion.

* fix(audit): report cleanup failures raised by member reads

A close failure during an embedded-package probe, member read or inventory
reaches the shared read filter as an ordinary reader failure. The filter kept
only cancellation and integrity, so the member became a skipped read, inspect
succeeded, and the retained descriptor was reported nowhere. MCP does not close
the service after each request, so no caller saw it.

Propagate any failure that carries cleanup, whatever its reason, so it reaches
releaseExposureFailure and the audit reports its retained resources.

* fix(audit): charge unsuccessful text reads to the byte allowance

An oversized, undecodable, or failed text read left textBytes unchanged, so
every member got the full per-member budget and the cumulative allowance did
not bound what a package decompressed. Count the bytes member reads produce
and charge them whatever the read yields. An oversized file on disk still
costs only its probe.

* fix(audit): bound the paths kept for HTML script targets

A base href is copied into every script target it resolves, so a page under
the text member allowance could retain hundreds of megabytes of paths. Charge
each new target's resolved path to a byte allowance and leave targets past it
unassessed.

* fix(audit): keep examining a page loaded as a script as HTML

A script element naming an .html member promoted that member to a
script, and the text pass then chose one reading. The page's own script
elements were never read, so an inline map in it was lost and a shipped
sourcesContent went unreported.

Run the script reading and the HTML reading independently, and list the
script target allowance among the cases that leave source map coverage
undecided.

* fix(audit): detect credential markers that end in a hyphen

The Google, GitLab, and Slack rules allow a hyphen in the token but ended
on a word boundary, so a token ending in a hyphen at the rule's minimum
length was missed and the audit reported the check as not observed.
Open-ended rules now take the whole token, and the fixed-length Google
rule checks the full token alphabet after its last character.

* fix(audit): confirm FAT identity for probed package members

The package probe treated a decoded FAT table as a universal Mach-O. A
JVM class past the 4 KiB probe shares the FAT magic, so its truncated
prefix decoded as a table, the member counted as an unread native image,
and Electron could not be ruled out of a complete scan.

Pass the member's declared size to the sniff and take identity from
classifyFatContainerEvidence. A member with no usable size, or with
slices past the probe, stays counted as possibly native.

* fix(audit): admit binary plists in package member probes

A package member's probe judged text by its own NUL test, which accepted
byte order marks but not the binary plist signature decodeMemberText also
reads. A PyInstaller executable shipping a binary Localizable.strings was
rejected without a scan or a skipped count, so a UTF-16BE private key
marker inside it could report not_observed.

Name the encodings that explain their own NULs in one predicate, and move
the plist fixture builders to module scope.

* fix(audit): locate Go findings only in images that retain build paths

The exposed decision required non-empty paths, but the locations took
every image whose build info read. An image with no paths was named as a
source of the leak beside the one that had them.

One predicate now decides both.

* fix(audit): keep completed findings when member cleanup fails

A descriptor close failure aborts the member scan, and the scan's state
was dropped with it, so the caller got a typed cleanup failure and no
report. Source maps already counted and credential hits already collected
were lost.

The reader now hands the service what it assembled, with coverage unknown
and a limitation saying so, alongside the failure. The service runs that
scan through its existing stopped-audit path: no later owner runs, and the
report goes on the error as its partial observation.

* fix(audit): keep coverage unknown for nested ASARs in package hosts

A member of a PyInstaller or .NET host reaches the binary probe as a
plain file. An ASAR among them has a NUL-bearing header, so the probe
dropped it with no traversal and no omission, and source map checks could
read not_observed with complete coverage.

Ask the ASAR owner's signature probe, and record a match as a nested
package that was not expanded, as inventory-classified ones already are.

* fix(audit): let native format facts outrank member names

examineMember tried the name-based text and map routes first, so a PE an
owner identified inside an ASAR, named payload.json, was read as text.
Its CodeView path and the packaged-native omission were both lost, and
native.pdb_path read not_applicable.

Route an owner-established native format to the native probe first, and
count an encrypted one as a packaged native image for the same reason.

* fix(audit): report relative paths given to compiler path flags

go.build_paths only kept rooted paths, so retained build settings like
CGO_CFLAGS=-Iprivate-sdk read not_observed even though the setting names
a directory.

Take the relative argument of a C compiler path flag, attached or in the
next field. Scope this to compiler settings, because Go's own tools use
the same letters for non-paths such as -B gobuildid and -T 0x1000.

* test(javascript): expect retained text on SVG inline scripts

The SVG test from main predates this branch's inlineScripts text field,
so each side passed alone and only their merge failed.

* fix(audit): keep an unresolved Electron host's fuses unknown

fuseCheck decided from whatever outcomes the images reported, so a host
with no fuse answer read not_observed as soon as any other image in the
directory reported safe fuses.

Pass the images named as the host into the report and leave a check
unknown when one of them has no answer for it. Other images stay free to
have no fuses, because helpers in a real Electron install have none.

* fix(audit): bound the paths kept for local source map targets

noteMapTarget kept every declared target resolved against its member's
directory, so a short URL under a long directory could retain far more
than the script's own size, with no cumulative limit.

Add the allowance noteScriptTarget already has. A target past it stays
unassessed and counts as a skipped map, and is never read.

* fix(audit): borrow the audit's scope admission for inventory

inspect holds resources.run for the whole audit, and inventoryArtifact
entered the same scope again. With failed cleanup pending from another
audit, the inner run waits for a drain that waits for the outer run.

Add inventoryArtifactInScope, which takes the caller's admission.

* fix(audit): run the exposure verifier's commands under supervision

The verifier launched npm and the CLI with raw execFile and then removed
its temporary tree, so a timed-out child could outlive the tree it was
using.

Use the shared verifier commands and close them before removing the tree.
Their exit error carries stdout on its snapshot, so the audit's exit-code
branch reads it from there.

* fix(audit): read relative -L paths and split flag arguments

The relative-path reader handled C compiler settings only, so a directory
kept through Go's own linker flag, such as -ldflags='-L private-sdk -s -w',
reported go.build_paths as not_observed.

A quoted flag and argument also reached it as one field. The separating
space stayed on the argument, which reported rooted paths twice and kept a
leading space on relative ones.

Give -ldflags its own pattern that matches only -L, so -B gobuildid and
-T 0x1000 stay ignored, and consume the separator in the flag patterns.

* fix(audit): keep embedded leaves beside schema-invalid map fields

The reader recovers independent indexed leaves only after a
SourceMapFormatFailure, but inspectSourceMap let the schema's ZodError
through. A sibling with mappings "!", a section with a bad offset or a
root with a bad field skipped recovery, so a valid leaf with
sourcesContent reported javascript.source_map_sources as unknown.

Normalize schema rejection in SourceMapFormat, which owns these parses,
so every structural failure reaches consumers as a format failure. The
message is unchanged, and the codec and web fetcher already map both
cases to the same result.

* fix(audit): keep packaged executables' own packages unknown

examinePackagedBinary counted a native image inside a package and moved
on, while only on-disk images reached the embedded-package probes. An
ASAR carrying a PyInstaller, Bun, Node SEA or .NET executable with a
source map therefore scanned complete and reported
javascript.source_map_sources as not_observed.

Run the same classifier on packaged images. The probes read carrier
offsets from a file, so the member's bytes stay in a temporary file for
the probe alone. A detected package keeps member coverage unknown, as a
nested ASAR does, and an image past one file's total byte allowance
stays unprobed and unknown. A plain native member leaves coverage intact.

* fix(audit): read an app's sampled images for fuses

A .app target sent only the bundle candidate to the fuse reader, which
resolves to the outer Electron Framework. A sidecar with RunAsNode enabled
was never read, so the check reported not_observed in the app view while
the directory view reported exposed.

Send the bundle candidate first and then the sampled native images. An
image whose digest matches the bundle's read is skipped, so the framework
is not read twice. A sidecar's safe fuses cannot settle the outer framework,
so a bundle read with no answer for a fuse keeps that check unknown.

* fix(audit): read a Node shebang as script identity

The path classifier gives an extensionless executable a resource kind,
so a script such as bin/runner with a `#!/usr/bin/env node` header was
scanned as text without its inline maps being examined or counted. A
complete inventory then reported no shipped sources for it.

The probe holds the first bytes a member reader sees, so establish the
identity there. The script keeps the usual outcomes: a map that is read
counts, and one past an allowance or undecodable stays unknown.

* fix(audit): keep linker strings out of Go build paths

Reading `-X 'main.usage=-L level'` as a library directory made
go.build_paths report `level`, so a plain string could fail an audit
under --fail_on low.

Read the optional package selector once, before the setting's first
flag, and keep the argument of -X attached to it, so a string it sets
is never scanned as another option.

* fix(audit): keep a failed probe directory removal owned

A packaged image's probe copies the member's full bytes into a temporary
directory. When removing it failed, the error became an ordinary
unreadable probe: the audit could still succeed, nothing could retry the
removal, and the client heard nothing about the leftover bytes.

Hold the directory in the reader's resource scope, so close retries it,
and report the unfinished cleanup without replacing a primary failure.

* fix(audit): keep a declared .map from reading as a linker map

A script's sourceMappingURL=assets.map was dropped because the name
already looked like a map. A malformed assets.map then settled as a
linker map, so both source-map checks read not_observed and
native.debug_companions reported an exposure the release does not ship.

Keep a generic .map name as a declared target and read it as a
JavaScript map, in either scan order, so it stays unresolved.

* test(audit): rethrow typed errors from release exposure checks

A failed Result was rewrapped in a plain Error, which dropped its type,
cause and cleanup details from the test output.

* fix(audit): match companions by member identity, not spelling

An ASAR member's location is relative, and a declared map target is
absolute, so a malformed assets.map read before z.js stayed a linker
companion. The examined-map lookup, which is normalized, then returned
early, leaving both source-map checks not_observed.

Index companions by memberLocationKey and keep each reported spelling
under it. Spellings stay distinct because symlinks can make them differ.

* fix(audit): retract only guessed linker maps on a declaration

A script declaring sourceMappingURL=symbols.pdb after the scan reached
symbols.pdb removed the shipped PDB from the companion list, because a
late declaration retracted any companion. native.debug_companions could
then read not_observed.

Keep name-identified companions in their own set, and retract only maps
that were read as non-JSON and guessed to be linker maps.

* fix(audit): record an unfollowed map link as skipped

The directory and ASAR readers do not follow links, so a symlinked
app.js.map dropped out before any map counter moved. Its target was
scanned as plain text, and both source-map checks read not_observed.

Count a link that names a map as skipped wherever entries reach the
audit, and mark it examined so a declaration does not count it twice.

* fix(audit): read relative paths in Go compiler and assembler flags

A binary built with -gcflags=-I private-sdk keeps that setting, but
relative parsing covered only C compiler settings and -ldflags, so the
directory dropped out of go.build_paths.

Give -gcflags and -asmflags their own path options, taken from what
go tool compile and go tool asm document, and strip the package selector
as -ldflags does. The assembler's -D defines a symbol, so it is not one.

* docs(audit): state the packaged image read allowance

Packaged ELF, Mach-O and PE images are read in full and copied to a
temporary file, sharing one max_file_bytes allowance, but the docs and
coverage.allowances still said only a PE gets more than a header read.

Name the allowance and the copies in docs/release-exposure.md,
RELEASE_EXPOSURE_ALLOWANCES and the max_file_bytes description.

* docs(audit): say how go.build_paths treats rooted -X values

A rooted -X value cannot be told from a file path, so the audit reports
it. State that, and why, so the rule is a contract rather than a side
effect of how flags are scanned.

* fix(audit): keep a declared sibling map out of the linker guess

When the selected file declared a sibling map, the sibling classifier
never consulted the declaration. A malformed assets.map then read as a
linker map and raised native.debug_companions.

Leave a declared sibling to the unreached-target accounting, as members
already are, so it stays an unresolved source map.

* fix(audit): keep undecodable Go and Rust metadata unknown

Unsupported meant two things: a container this reader does not handle,
and metadata the image holds in a form the reader cannot decode, such as
a compressed .dep-v0 section. Both read as not_applicable, so an image
that carries metadata reported that the check did not apply.

Mark the second case on the format failure and carry it on
AnalysisUnsupportedTargetError, so the audit records a failed read and
stays unknown. The tools' own errors and messages do not change.

* fix(audit): keep coverage unknown for a PE header past the probe

A packaged member whose PE signature lies beyond the probe prefix stayed
unclassified, so the embedded-package probe never ran and coverage stayed
complete. A map inside such an image then read as not_observed.

Record the member as an unexamined package where it is already marked as
a possible native image.

* fix(audit): read the scripts of a standalone SVG

An SVG can carry a script with an inline source map, but only .html and
.htm members reached the script owner, so such a file left every map
counter at zero and javascript.source_map_sources read not_observed.

Treat .svg as a script-bearing page. htmlArtifactReferences already
reads SVG script elements, so this adds no parsing.

* fix(audit): keep every possible host when no name picks one out

With no named Electron host, every sibling ranks alike and
electron_host_paths stayed empty, so one image's safe fuses settled the
checks even when another possible host reported none.

Keep all sampled candidates as hosts in that case, so a candidate
without fuses leaves the checks unknown.

* fix(audit): read a text-named native member by its bytes

A PE member called payload.json took the text route, so it reached no
native probe, and its CodeView path and packaged-native uncertainty were
lost.

Decide from the bytes the text read already loaded, so real text costs
no extra read and the map and text allowances stay as they were.

* fix(audit): let a map or script read identify a native member

A package lists names without formats, so a PE called payload.js.map
took the map route and never reached a native probe. Its CodeView path
was lost, and with nothing counted as failed, native.pdb_path could read
not_applicable. A PE at a declared script or map target was lost the same
way, because declaration overrode the route.

Decide from the bytes the route already read, in the map route as in the
text route, and keep the listed kind for a promoted script target. Real
text and maps cost no extra read, so those allowances are unchanged.

* fix(audit): name a symlinked sibling debug companion

Selecting an executable skipped every symlink beside it before the name
check, so an app.pdb link left no companion path and no skip count, and
native.debug_companions read not_observed. Selecting the directory
reports the same link.

Check the name first. The map branch already needs a regular file, so no
link is followed.

* fix(audit): keep map coverage unknown for a linked script

A link named app.js to an extensionless payload was skipped as a link,
and its target was read as plain text with no map directives parsed. An
inline map there left no parsed or skipped count, so
javascript.source_map_sources read not_observed. Only links with a map
name were counted.

Count a link under any name that can declare a map, the set an unread
member already uses. It stays one omission when a page or a directive
also names it. The target is still not followed.

* fix(audit): keep a declared JavaScript map beside a selected file unknown

A local sourceMappingURL with a JavaScript map name such as app.js.map
was dropped on the assumption that it would be examined as a member.
When the selection is a file, sibling .js.map files are never read, so
the declared map left javascript.source_map_sources not_observed while
the same target spelled as a file: URL left it unknown. Record every
local target so an unreached one counts as skipped.

* fix(audit): read paths a C compiler forwards to the linker

CGO flags can hand linker options through -Wl,a,b or -Xlinker a. The
compiler flag parser saw -Wl,-L,private-sdk as one field and missed the
relative library directory, so go.build_paths reported not_observed.

Split forwarded options out before reading paths and read them with the
linker's own path options (-L, -T, --library-path, --script). This also
stops -Xlinker -Bsymbolic from being read as the compiler's -B prefix.

* fix(audit): read forwarded linker options by their linker meaning

The forwarded linker flag pattern read -Ttext=0x400000 as a script path
named text=0x400000, and it ignored -I, so -Xlinker -Ilib/ld-custom.so
lost the interpreter path the compiler flag pattern found before the
split.

Exclude the -T section and segment address options (bss, data, text,
text-segment, rodata-segment, ldata-segment) and read -I and
--dynamic-linker as paths.

* fix(audit): split Go flags as Go does and read external linker flags

Go splits a flag value with quoted.Split: a field that starts with a
quote runs to the matching quote, and any other quote is a literal
character. The reader stripped every quote, so CGO_CFLAGS
-I"/home/O'Neil/include" reported /home/ONeil/include, while cc was
given the directory "/home/O'Neil/include", quotes included.

-ldflags kept -extldflags values with the Go linker flags, where only
-L counts, so -extldflags=-Lprivate-sdk reported no path. The values
are now read as the C compiler driver reads them, including -Wl, and
-Xlinker forwarding.

A package member named payload.json or payload.js.map that holds an
ASAR took the text or map route, which checked only native signatures,
so the nested archive was scanned as text and coverage read complete.
The route's own read now recognizes the ASAR signature as well, and the
nested package leaves coverage unknown.

* fix(audit): keep a named script beside a selected HTML file unknown

A script element whose target had a JavaScript name such as app.js was
dropped on the assumption that it would be examined as a member. When
the selection is an HTML file, sibling scripts are never read, so a
sibling holding an inline map left javascript.source_map_sources
not_observed while the same target named app left it unknown. Record
every local script target so an unreached one counts as skipped.

* fix(audit): read a retained -pgo setting as one profile path

`-pgo file` names a single profile, and under -trimpath Go keeps only
its file name, so the flag readers dropped values such as
`private-training.pgo` and split paths with spaces or commas. Report
the value whole, skipping `auto` and `off`.

* fix(audit): read -extldflags paths as the external linker splits them

With -ldflags set to -extldflags '-L "/srv/private sdk"', the outer
flag reader could not match the path behind the inner quote, and the
external reader kept only relative paths, so go.build_paths read
not_observed. Read each -extldflags value once, as go tool link splits
it with quoted.Split, and collect rooted and relative paths from those
same arguments. The rest of the setting is read as before.

* fix(audit): read the Go linker's other path options

go.build_paths read only -L as a Go linker path option, so a retained
-ldflags='-tmpdir=private-obj -s -w' reported no path and --fail_on low
missed the directory.

Read every option `go tool link -h` documents as taking a directory,
path or file: -I, -L, -benchmarkprofile, -capturehostobjs, -cpuprofile,
-fipso, -importcfg, -memprofile, -o, -r and -tmpdir. -extld, -extar and
-libgcc name a program or `none`, so only their rooted values count.

* fix(audit): keep a quoted Go argument whole

goFields already reads -L "/tmp/private - sdk" as two arguments, but
each field was then split again before every whitespace-led dash, so
go.build_paths reported /tmp/private instead of the retained directory.

Split again only a field that begins with a flag, after any package
selector, as a quoted "-L dir -s -w" does. Any other field is one
argument and is kept whole, in -ldflags, -extldflags and CGO flags.

* fix(audit): keep coverage unknown for an unread packaged image

When a listed package member is a native image but its first read
fails, examinePackagedBinary returned before probePackagedImage could
mark its possible embedded package unexamined. Coverage stayed complete
and javascript.source_map_sources could report not_observed for a
payload nobody read.

Mark member coverage unknown wherever a native member goes unread,
including an encrypted one, as an image the probe could not read
already does, and say so in the limitations.

* fix(audit): read rooted Go tool path arguments whole

`-L "/tmp/private,sdk"` and `-L=/tmp/a=b` name one directory, so a `,` or
`=` in a documented path flag's argument is literal. `-trimpath` keeps its
prefix=replacement structure.

* fix(audit): count an unread packaged member once

* fix(audit): keep trimpath rewrites and directory fields apart from selectors

* fix(audit): keep a .map unsettled once a declaration was discarded

* fix(audit): skip env option arguments when reading a Node shebang

* fix(audit): keep companion coverage unresolved for a generic .map link

* fix(audit): count a dropped linker-map guess as a skipped companion

* fix(audit): read env settings after -- in a Node shebang

* fix(audit): route a selected native image named .map by its format

A directly selected or listed native image has no package format, so
examineMember skipped its native-first branch and sent a file such as
release.map through map routing. Its bytes then became a linker-map
guess and marked native.debug_companions exposed. Native format now
outranks the name for every member, packaged or not.

* fix(audit): read forwarded -rpath and -rpath-link as linker paths

A cgo build with CGO_LDFLAGS=-Wl,-rpath,private-runtime kept the
relative directory, but the forwarded linker parser only knew -L, -T,
-I, --library-path, --dynamic-linker and --script, so go.build_paths
reported not_observed. Add -rpath and -rpath-link to the same parser,
in the attached, = and next-field forms. Loader-expanded values such as
$ORIGIN/../lib and @loader_path/Frameworks name no build-host directory
and stay unreported.

* fix(audit): read each entry of a forwarded linker search list

-rpath and -rpath-link take a colon-separated search list. The loader
token filter tested the whole list, so -Wl,-rpath,$ORIGIN:/opt/private-runtime
dropped /opt/private-runtime along with $ORIGIN, and the rooted-path pass
cannot see an entry that follows a colon. Split each search list into its
entries and drop only the loader-expanded ones.

* fix(audit): expand loader tokens only in a linker search list

The runtime loader expands $ORIGIN and @loader_path only in a -rpath or
-rpath-link search list. The token filter also ran on the argument of -T,
-L and -I, so -Wl,-T,$ORIGIN.ld dropped a literal linker script path and
a complete read could report go.build_paths as not observed. Search list
entries keep the filter; every other linker path argument is kept as is.

* fix(audit): count a .map link beside a selected file as a skipped companion

The sibling scan skipped any .map entry that was not a regular file, so a
symlink such as symbols.map -> payload beside a selected executable left
debug companion coverage at not observed. The reader never follows links,
so the linker map such a link may name is unread, not absent. Count a
generic .map sibling link as a skipped companion without following it,
as noteUnfollowedMapLink already does inside a selected directory.

* fix(audit): split a linker search list only for an ELF image

Only the ELF linker reads a -rpath or -rpath-link argument as a
colon-separated list. The Mach-O linker stores each -rpath argument as one
directory, so -Wl,-rpath,@loader_path/lib:extra is a single loader-relative
entry, yet it was split and extra was reported as a build path. Carry the
native image format into the Go flag reader and split search lists only for
ELF; other formats read the whole argument as one entry.

* fix(audit): expand only the image loader's runtime tokens

A runtime search path token is expanded only by the loader of the image's
format. The ELF loader expands $ORIGIN, $LIB and $PLATFORM; dyld expands
@executable_path, @loader_path and @rpath. Each is literal text to the
other loader, so -Wl,-rpath,$ORIGIN:/opt/private-runtime on a Mach-O image
names a literal directory, yet it was dropped and go.build_paths reported
not_observed. Pick the token set from the native image format; the PE
loader reads no search list, so it expands none.

* fix(audit): keep a declared .map link beside a selected script a source map

The sibling scan counted any .map symlink as a skipped debug companion
before checking whether the selected script declared it as its source
map. classifySiblingMap already excludes a declared target that is a
regular file, so a declared symbols.map link added a false unknown to
debug_companions. Apply the declared-target check before either file-type
branch; noteUnreachedTargets still counts the unread map as skipped.

* fix(audit): byte-check members whose kind is only an inventory path role

Inventory gives a packaged member a role from its name, such as resource
for payload.json or source-map for payload.map. The packaged-format guard
treated any kind other than "file" as owner-stated, so a nested ASAR an
ASAR lists under one of those names skipped signature recognition and
cleared coverage. Track whether the package owner stated the kind (Bun
sourcemap region, SEA main script) and only skip the byte check then.

* fix(audit): read Go's linker -r setting as a runtime search list

go tool link -r sets the ELF dynamic linker search path, dir1:dir2:...,
which the linker writes to RUNPATH. It was read as one whole path, so a
release built with -ldflags='-r=$ORIGIN/../lib' reported the
loader-relative entry as a build path and go.build_paths as exposed.
Read -r entry by entry with the same image-loader-aware split and token
filter a forwarded -rpath uses: ELF splits at ':' and drops $ORIGIN
entries; Mach-O keeps the argument as one directory.

* fix(audit): keep a selected package executable's native carrier format

A directly selected PyInstaller executable named release.map recorded the
package format as its member format, even though its ELF or PE carrier was
already recognized. examineMember then missed the native-first branch and
treated the executable as a linker map, reporting a debug companion.
Forward the native carrier format for the selected file, as listed members
already do.

* fix(audit): keep coverage unknown for an unread unclassified member

A packaged member with no format of its own, such as a PyInstaller
payload.bin, recorded only a skipped text member when its read failed
before the header. Member coverage then stayed complete, though the
member could be a native image or carry a nested package. Leave
coverage unknown and say which member could not be read, as an unread
native member already does.

* fix(audit): keep ASAR header admission distinct from max_file_bytes

A directory holding an ASAR whose header exceeds the header admission
was reported as a file-size refusal with max_file_bytes advice, which
cannot fix it. Report the header admission as its own bound instead.

* fix(audit): thread the shared ASAR header admission into release exposure

createReleaseExposureService built ReleaseExposureReader with no admission,
so its inventory and every reopened ASAR reader stayed on the 16 MiB
default and ignored REA_ASAR_MAX_HEADER_BYTES and asar.max_header_bytes.

Resolve the admission in the composition with runtimeAsarHeaderAdmission,
as the Electron and V8 observation providers do, and pass it to both
inventoryArtifactInScope calls, the reopened container readers, and the
nested ASAR signature probe. The refusal now names the setting that can
raise it instead of claiming there is no runtime override.

* fix(audit): keep a nested ASAR probe stopped at the admission undecided

The release exposure reader treated an ASAR signature probe that stopped
at the header admission as "not an ASAR". A packaged member such as
payload.json holding an ASAR whose JSON starts past asar.max_header_bytes
was then read as plain text, its inner maps went unread, and
javascript.source_map_sources could stay not_observed.

A probe that reaches the admission limit, or runs out of a prefix read
after its extents parse, now marks package coverage unknown and records
a limitation, while the member is still examined as text.

* fix(cli): parse audit-release-exposure --max-file-bytes as a decimal token

Main now requires every numeric CLI option to admit decimal tokens and
reject 0x10 and 1e3 (tests/boundary/cli/cliDecimalArguments.test.ts).
Wrap the release exposure maxFileBytes option in cliDecimalField like the
other maxFileBytes options, so the domain field keeps its bounds and default.

* docs(specs): add the release exposure audit specification

Add spec.md, plan.md and tasks.md under docs/specs/artifacts/release-exposure/
as docs/development/documentation.md requires for a new feature, and list
them in docs/specs/README.md. The existing guide at docs/release-exposure.md
keeps describing current behavior and its route is unchanged.

* fix(audit): bind unpacked ASAR member reads to their inventoried digests

A selected ASAR is captured and checked against its inventoried digest, which
binds packed members. An unpacked member is read from its .unpacked companion
on disk instead, and readContainerMember and readEntryBytes never compared
those bytes with the member digest inventory recorded, so a companion
replaced between inventory and the scan was accepted.

readContainerMember now passes the member's inventoried digest for unpacked
entries, and readStreamBounded hashes the full stream, draining past the read
allowance when needed, and fails with "ASAR member changed before
interpretation", which surfaces as an artifact_changed error like the
container check. A regression test replaces a companion after inventory. The
guide, spec, plan and tasks describe the binding.

* fix(audit): let stated JavaScript roles outrank a .map name

isDeclaredMap() routed any member whose name ended in .map to map
handling before the owner's stated role or a page's script declaration
was considered. A Node SEA main script at boot.map, which the SEA reader
marks as javascript, and a .map script an HTML file loads both became
linker-map guesses, so their inline maps were never read and
native.debug_companions was reported exposed.

An owner-stated source-map role or a declared map target still makes a
member a map. Otherwise a stated javascript kind or a script target now
wins, and only then does the .map suffix, or the inventory path role that
repeats it, decide. Regression tests cover both cases, and the guide
notes that a SEA main script is read as a script whatever it is called.

* fix(audit): keep coverage unknown when a packaged text member fails to read

A compressed member named as text, such as payload.json in an unexpanded
PyInstaller host, can fail inflation before any header is returned. The
.bin case already leaves member coverage unknown because an unread header
may hide a nested image or package, but examineTextMember() only counted
the member as skipped text, so otherwise complete coverage stayed
complete and package-dependent checks could settle.

An unread packaged member whose kind its package did not state now goes
through noteUnreadBinary(), which counts it as skipped text, marks
coverage unknown, and records the limitation. A regression test corrupts
the zlib stream of payload.json in a CArchive host and checks the intact
member keeps coverage complete. The guide describes the rule.

* fix(audit): keep the unpacked ASAR digest check inside the read allowance

ea87cf8 made readStreamBounded hash an unpacked ASAR companion to its end
so it could compare against the inventoried digest, which drained the
stream past the read allowance. A 1 GiB unpacked module was read in full
for a 4 KiB header probe and again for a package probe, and a companion
that grew after inventory had no byte limit on that path.

readStreamBounded now stops at the allowance as every other member read
does, and compares the digest only when the member ended within the
allowance. A companion past the allowance stays over_allowance without a
digest verification. A test counts the bytes each scan read pulls from an
8 MiB companion. The guide and spec state the bound.

* fix(audit): read a script target whose bytes are a source map as a map

1d3f89f let a stated javascript kind or an HTML script target outrank a
.map name, so an HTML `<script src="main.map">` that points at a valid
source map with sourcesContent was read only as a script. Its sources
were never counted and javascript.source_map_sources could miss an
exposed finding.

A script role says how a member is loaded, not what its bytes hold. A
.map member or loaded script whose text parses as a source map, a JSON
object with version and mappings, sources, or sections, is now read as
a source map. Anything else is still read as a script, so a SEA main
script named boot.map and a .map script carrying an inline map keep
reaching examineInlineMaps. A regression test covers the map case, and
the guide states the rule.

* fix(audit): retract a linker-map guess when a page loads the map as a script

noteScriptTarget() returned early for a target already read, counting its
source-map coverage as skipped but leaving any linker-map guess the first
read recorded. When a directory or ASAR listed a non-JSON app.map before
the index.html that loads it with <script src="app.map">, app.map stayed in
native.debug_companions and the result depended on enumeration order.
noteMapTarget() already retracted such a guess for a late map declaration.

A late script declaration now retracts the guess too, and still counts the
script as skipped because it was never examined as JavaScript. A test
covers the directory and ASAR order, and the guide states the outcome.

* fix(audit): respect script and native identity for sibling linker maps

classifySiblingMap() guessed a linker map for any sibling .map whose bytes
could not begin a JSON object. A sibling boot.map that a selected index.html
loads with <script src="boot.map">, or an ELF image named release.map beside
a selected executable, was therefore reported in native.debug_companions.

scanSiblings() now skips a sibling that is a script target as well as a map
target, leaving noteUnreachedTargets() to count it, and classifySiblingMap()
records no guess when sniffNativeFormat() recognizes the header. Tests cover
both cases and the guide states the outcome.

* fix(audit): read a script-loaded map from the text that recognized it

scriptHoldsMap() recognizes a .map or script target as a source map from
decodeMemberText(), which honors a UTF-16 byte order mark, but
readScriptAsMap() then passed the raw bytes to readSourceMap(), which reads
UTF-8. A BOM-marked UTF-16 main.map loaded by <script src="main.map"> failed
the second parse and its prefix became a linker-map guess.

readScriptAsMap() now hands the decoded text to readSourceMapText(), which
readSourceMap() also uses for byte input, so both stages read the same text.
A test covers a UTF-16 map with embedded sources.

* fix(release-exposure): honor byte order marks in binary plist data

A binary plist data object holding BOM-marked UTF-16 text was decoded as
UTF-8, so credential markers in it went unmatched while coverage still
read complete. Decode such payloads with the encoding their mark declares,
and leave coverage incomplete for UTF-32, which no reader scans.

* fix(audit): keep an unprobed package member past the text allowance unknown

Once the cumulative text allowance was spent, examineTextMember() counted a
packaged member named like text, such as payload.json in a .NET or
PyInstaller bundle, as skipped text without reading its header. Coverage
stayed complete even though the member could be a native image or a nested
ASAR, while the same bytes named payload.bin kept coverage unknown.

An unexpanded package member whose kind the owner did not state now leaves
coverage unknown with a limitation when the allowance skips it, the same
uncertainty an unread .bin member carries. No extra read is made, so the
allowance still bounds what is opened. A test covers an ASAR with an
embedded source map named payload.json after sixteen full text members.

* fix(release-exposure): require a read host for every Electron layout

A directory with several resources/app.asar layouts counted one named
host as fuse coverage for all of them. Track host coverage per layout so
safe fuses stay unknown when a whole layout's possible hosts were
dropped from the native image sample.

* fix(release-exposure): keep a .map script past the target allowance unassessed

When HTML script references spent the resolved-path allowance, a later
.map script target was dropped without its script role, so its JavaScript
bytes settled as a linker-map guess and debug_companions reported an
exposure. A refused script target now sets mapTargetsDiscarded, the same
flag a refused map target sets, so a later .map stays unassessed instead
of becoming companion evidence.

* fix(release-exposure): read a Node shebang as script in any text member

A selected or listed text file such as runner.txt that starts with a Node
shebang went straight to the text reader, which never gave it a JavaScript
role, so its inline map went unread and source_map_sources read
not_observed. The text reader now takes the role from the bytes it read,
so every route into it sees the shebang, and the probed-text path no longer
sets the role itself.

* fix(release-exposure): exclude Go search lists by position, not by value

compilerPaths dropped every literal path whose text equalled a forwarded
-rpath or -rpath-link list, so CGO_LDFLAGS=-Wl,-L,$ORIGIN -Wl,-rpath,$ORIGIN
lost the literal -L directory and go.build_paths read not_observed. The
literal flags are now read with each search list option and its argument
removed by position, and the search lists are still read entry by entry.

* fix(release-exposure): keep a probed Node launcher's role past the allowance

The probe recognized a Node shebang but handed the text reader the member
without its JavaScript role. Once earlier text spent the shared allowance,
the text reader returned before reading the header again, so an
extensionless launcher added no skipped map and both source-map checks read
not_observed instead of unknown. The probe now passes the role it found.

* fix(release-exposure): decode a map in the encoding its byte order mark names

The map reader, its prefix check and the sibling classifier decoded map
bytes as UTF-8, so a generic symbols.map holding BOM-marked UTF-16 source
map JSON failed to parse and its prefix read as a linker map: a spurious
debug companion and source_map_sources not_observed. Map bytes are now
decoded by their byte order mark at that boundary, and a UTF-32 map, which
this reader does not decode, stays unresolved.

* fix(release-exposure): keep a lone dash inside a quoted Go path argument

splitGoField split a quoted field before every whitespace-led dash, so the
retained -ldflags '-s "-L=/tmp/private - sdk" -w' read the dash inside the
directory as a new flag and goPaths reported /tmp/private. A quoted field
now splits only before a dash that begins a flag, so a dash followed by a
space stays part of the path.

* fix(release-exposure): choose possible Electron hosts within each layout

The named-or-possible host choice ran across every kept image, so a named
a/a.exe removed the unnamed b/launcher.exe of a second ASAR layout from
electron_host_paths, and its unresolved fuses no longer held
electron.run_as_node at unknown. Each layout now chooses its own named or
possible hosts, and the sets are combined.

* fix(release-exposure): read a relative compiler --sysroot as a build path

The compiler path flag list had no --sysroot, and the rooted-path fallback
cannot see a relative operand, so CGO_LDFLAGS=--sysroot=private-sdk left
go.build_paths at not_observed. --sysroot is now a compiler path flag,
attached with = or given as the next field.

* fix(release-exposure): recognize Windows Electron layouts by the owner's path

The desktop owner gives each file a native local_path, such as
C:\ship\App\resources\app.asar on Windows, and desktopPathConventions only
splits on /, so every ASAR basis was skipped: host ranking and
electron_host_paths stayed empty and a named host with no sentinel let a
safe helper read electron.run_as_node as not_observed. Each basis now
carries whether the owner's slash-separated path is an ASAR layout, and the
layout folders read a Windows absolute path with Windows separators on any
host.

* fix(release-exposure): read a Bun module by its JavaScript loader

An extensionless Bun contents member such as $bunfs/root/release carries
loader_name js, but ownerStatedKind kept only the sourcemap region, so the
member was never read as a script and its inline map went unseen. Give a
Bun contents region the javascript role for the js, jsx, ts and tsx
loaders, as SEA main scripts get it.

* fix(release-exposure): read a Node shebang .map as a script

An undeclared `.map` was routed as a source or linker map before its
first bytes were checked, so a Node script named `runner.map` recorded a
linker-map companion and never had its inline map read. When the map
read of an undeclared `.map` starts with a Node shebang, its map slot is
returned and the text reader examines it as JavaScript, so its inline
map is parsed and no companion is reported.

* Detect full-length GitHub refresh tokens

Narrow github.token to gh[opus]_ with 36 characters and give ghr_ its
own 76-character github.refresh_token rule, with a test.

* fix(release-exposure): bind a negative package probe to the inventoried image

An on-disk native image whose embedded-package probe finds nothing clears member coverage, so it now hashes the probed descriptor and compares it with the inventoried digest before accepting absent. A replacement after inventory fails as a changed member instead of reporting complete coverage.

* fix(release-exposure): let an app bundle read settle only its own layout

An app target, an app-bundle fuse read or a sampled selected executable used to mark the Electron executable as included for every ASAR layout, so safe outer fuses reported not_observed even when a nested Electron app's host was never read. Those reads now settle only their own layout; every other discovered layout still needs its own host read, so an omitted nested host keeps fuse checks unknown.

* fix(release-exposure): honor a Node shebang before guessing a sibling linker map

A sibling .map beside the selected file that starts with a Node shebang is a script, as it already is for a member .map, so classifySiblingMap no longer records it as a linker-map debug companion.

* fix(release-exposure): name a bundle's own layout by the owner's path

The settled layout set joined the caller's bundle spelling with Contents, so a selected /tmp/App.app missed the owner's resolved /private/tmp/App.app/Contents layout and safe fuse checks fell to unknown. Each archive basis now carries the owner's relative layout folder, and the bundle's own layout is the one whose relative folder is Contents, looked up by the owner's resolved folder.

* fix(release-exposure): read file-valued linker options as build paths

LINKER_PATH_FLAG listed --script but not the other linker options that read a file, so -extldflags=-Wl,--version-script=private-versions.map left go.build_paths not_observed. --version-script, --dynamic-list and --retain-symbols-file now read their operand as a path, attached or in the next field.

* fix(release-exposure): count a macOS bundle's framework and MacOS hosts in its layout

A .app layout resolves to its Contents folder, but its hosts are Contents/MacOS/* and the Electron framework binary under Contents/Frameworks, so per-layout host ranking and fuse coverage dropped them and one bundle's safe fuses could settle another bundle. Those images now belong to the bundle's layout, with the framework binary ranked as its host.

* fix(release-exposure): keep a bundle's launcher out of its fuse hosts

A macOS bundle's Electron Framework binary carries its fuses. When it
is among a layout's named hosts, it is now the layout's only fuse host;
the MacOS launcher beside it is still sampled but no longer leaves safe
fuse checks undecided when it has no sentinel.

* fix(release-exposure): keep an undecided PE package member out of linker maps

An unexpanded package member named .map whose PE signature lies past the
retained prefix fell through to noteUnreadMap, which read its MZ prefix as
a linker map and reported native.debug_companions exposed with complete
coverage. The map route now records the same undecided-PE uncertainty the
binary route does, so the member stays unexamined and coverage unknown.

* fix(release-exposure): scan the text of an undecided PE map member

A packaged .map member whose MZ header is undecided returned before its
fully read bytes reached the credential scanner, so a PEM key in it read
as unknown instead of exposed. Scan bytes read in full before returning,
while still keeping PE coverage undecided.

* fix(release-exposure): keep an oversized text member's undecided PE header

A packaged text member past the per-member allowance keeps only its prefix,
so an MZ header whose PE signature lies further in returned null and the
text route dropped the native and package uncertainty. Note the undecided
header as the map and binary routes do.

* fix(release-exposure): join Electron layouts by caller path and keep partial discovery unknown

The desktop owner resolves its root, so a target selected through a
symlinked parent got canonical local_paths that never matched the
audit's caller-spelled images, and the named host dropped out of
electron_host_paths. Rebase owner paths under the canonical root onto
the selected path.

When discovery stopped after it found one Electron layout, the basis
list was treated as exhaustive. Report the layouts as incomplete so safe
fuses on the listed host stay unknown.

* fix(release-exposure): keep Electron hosts under a trailing-slash selection

Join the rebased archive path with appendCallerPath so a selected
`/work/app/` yields `/work/app/resources/app.asar`, whose layout folder
matches the inventoried executables' parent.

* fix(release-exposure): keep maps unassessed for unread text past the allowance

A text-named member such as `runner.txt` can start with a Node shebang.
Once the text allowance is spent its header is not read either, so count
every unread member that could hold a script as a skipped map instead of
trusting the filename-derived role.

* fix(release-exposure): keep an unread packaged map member undecided

When a packaged map member yields no header (a failed read or no map
slot), its name is no evidence of its format. Route it through
noteUnreadBinary, as the text route does, so a member that may be a
native image or package keeps coverage unknown.

* fix(release-exposure): keep an undecided sibling PE header out of linker-map guesses

A sibling .map past the map allowance keeps only its probe, so an MZ
header whose PE signature lies further in is undecided. Share the
undecided-PE test with packaged members and count such a sibling as a
skipped companion instead of guessing a linker map.

* fix(release-exposure): keep possible fuse hosts when no ASAR layout names one

Without an ASAR basis no image is ranked, so no image counted as a fuse
host and one image's safe fuses could settle a sentinel-less sibling.
While the desktop family is undecided and no layout names a host, treat
every sampled image as a possible host.

* fix(release-exposure): keep images outside a found ASAR layout as possible fuse hosts

An asar: false build beside a discovered ASAR layout gets no basis, so
its executables were never fuse hosts and the layout's safe fuses could
settle a sentinel-less image in the other build. When no name picks out
a host outside the found layouts, treat every sampled image there as a
possible host, as the no-layout case already does while the family is
undecided.

* fix(release-exposure): keep a dropped host outside a found ASAR layout unread

An asar: false build beside a found ASAR layout names no host, so an
executable outside every layout that the native-image sample dropped may
be that build's unread fuse host. Coverage no longer counts as proven
when such an image was inventoried but not sampled.

* fix(release-exposure): read Go build-path response files named in compiler and linker flags

A CGO setting such as `-Wl,@/tmp/private-link.rsp` or
`-Xlinker @/tmp/private-link.rsp` names a response file the linker reads
options from, and a bare `@FILE` is read by the compiler driver. Report
the file's path, relative or rooted like other path arguments, without
opening it. An option's own value, such as `-install_name
@rpath/libx.dylib`, and dyld runtime tokens are not response files.

* fix(release-exposure): keep a dropped host outside an ASAR layout unread whatever its name

The guard for a sampled-out host beside a found ASAR layout ruled images
out by filename, so an asar: false host named like `b/My.App` was missed
and safe fuses from the sampled layout settled the installation. Every
inventoried native image outside the found layouts that the sample
dropped now keeps the fuse checks unknown.

* fix(release-exposure): keep maps unassessed for an unreadable extensionless launcher

An unclassified file whose initial header probe fails, such as an
extensionless `runner` that became unreadable after inventory, may carry
a Node shebang and a map directive. The probe failure now leaves its
maps unassessed, as an unread `runner.js` or packaged member does,
instead of counting the file as holding no script.

* fix(release-exposure): read a response file named like a dyld token

`-Wl,@rpath/private-link.rsp` names a linker response file, not a dyld
runtime path, so dropping every `@rpath` or `@loader_path` argument hid
it. A dyld token is now skipped only as the value of an option that takes
one, such as `-install_name` or `-dylib_file`.

* fix(release-exposure): keep the partial member scan on ordinary cancellation

A scan cancelled after some members were examined reported its partial
scan only when cleanup also failed, so a clean cancellation dropped the
recovered source-map and credential observations. A cancelled scan now
hands its assembled facts to the interrupted-scan visitor with unknown
coverage and a cancellation limitation, and the caller still receives
the cancellation error carrying that partial audit.

* docs(release-exposure): describe the partial report kept when the member scan is cancelled

Since the member scan keeps what it examined on ordinary cancellation,
the status guide no longer says that cancelling the scan returns no
partial report. It now describes the partial observation and limits the
no-report case to cancellation before the scan can be assembled.

* fix(release-exposure): keep maps unassessed when a text header is cut short

A text-named member read with one or two bytes of the text allowance
left returns only `#` or `#!`, which cannot establish a Node shebang, so
`runner.txt` kept its non-script role and its unexamined inline map was
treated as absent. A partial or missing header that could still start a
shebang line now leaves the member's maps unassessed, as an unread
member's do once the allowance is spent.

* fix(release-exposure): keep dotted executables as possible hosts in ASAR layouts

Inside an ASAR layout, a sibling whose name had a dot and no `.exe`
suffix, such as `my.app`, was neither ranked for the native sample nor
counted as a possible Electron host. With shared libraries filling the
sample, that image could be dropped while the layout still counted its
host as read, so safe fuses on a helper reported `run_as_node` as not
observed. A dotted sibling that is not named like a shared library is
now sampled after plain siblings and counted as a possible host.

* fix(release-exposure): split env -S shebangs on unquoted blanks only

The env shebang reader split the whole line on spaces before handling -S,
so a launcher such as `#!/usr/bin/env -S NODE_OPTIONS='--conditions
development' node` named `development'` as its program and its maps were
never assessed. An -S operand now takes the rest of the line and is split
the way GNU env -S splits it: single and double quotes, backslash escapes,
\_ and \c, and a leading # comment. A string env itself rejects, such as an
unterminated quote, runs no program.

* fix(release-exposure): keep owner facts completed before cancellation

When a Go, Rust, fuse or desktop owner finished its Evidence and then saw
cancellation, completeProviderEvidence returned that Evidence as the
error's partial observation, but the audit only kept the error and
substituted a not-run fact, so a recovered Go build path or Rust package
list reported unknown. Each owner's Evidence-to-fact reading now lives in
one function used for both a successful read and a completed partial, so
the partial passes the same schema and inventory digest checks before its
fact is kept. Later owners still do not run.

* fix(release-exposure): keep env -S programs named by a variable unassessed

`env -S` expands `${NAME}` from the environment, so a launcher such as
`#!/usr/bin/env -S ${NODE_BIN}` may run Node, but the shebang reader kept
the placeholder as the program name and treated the file as plain text,
reporting its inline map as absent. The env splitter now marks fields that
expand a variable, and a program whose name holds one leaves the shebang
undecided: the member's maps count as unassessed, and a `.map` companion
with such a header is not taken as a linker map. Quoted or escaped `${`
stays literal, and a resolved basename such as `${DIR}/bin/deno` still
decides.

* fix(release-exposure): keep the ASAR header admission for selected files

A directly selected app.asar whose header exceeds asar.max_header_bytes
now returns the typed header admission refusal and its recovery, as the
directory scan already does, instead of a generic partial report.

* fix(release-exposure): keep a map member with a variable env -S program unassessed

An undeclared `runner.map` whose `env -S` program is `${NODE_BIN}` may
run Node, so `examineMapMember` now treats an undecided `nodeShebang`
result as an unassessed map and companion, as `classifySiblingMap`
already does, rather than falling through to a linker-map guess.

* fix(release-exposure): read a forwarded -Map linker map path

splitForwardedLinkerFlags hands -Wl,-Map=private-link.map to the linker
side, but LINKER_PATH_FLAG had no Map entry, so the relative map path was
dropped and go.build_paths read not_observed. Add Map to the file-valued
linker options so -Map=FILE and -Map FILE are both reported.

* fix(release-exposure): count unread text in undecided map branches

An oversized .map whose shebang names its program with an unresolved
variable, or a package member with an undecided PE header, returned
before counting its unscanned text, so credential markers could read
not_observed. Count any non-bytes read as skipped text in both branches.

* fix(audit): bound map reads and preserve linker paths

* fix(audit): retain unread package uncertainty

* fix(audit): retain ambiguous linker operands

* fix(audit): preserve compiler path separators

* fix(audit): retain distinct linker map occurrences

* fix(audit): apply map declarations to their exact source path

A declaration recorded only its NFC lookup key, so every member or
sibling sharing that key took the declared JavaScript-map route. On a
normalization-sensitive filesystem an unrelated linker map spelled in
another form lost its companion evidence when the declaring script was
scanned first.

Record the resolved spellings each declaration writes and treat only an
exact match as declared. Another spelling in the bucket is read as an
undeclared map; when no declared spelling is reached, its linker-map
guess stays unresolved coverage rather than companion evidence.

* fix(audit): keep partial inventory from settling fuse-host coverage

When a directory read found ASAR layouts but the inventory stopped
early, a separate asar: false build outside those layouts may never
have been listed. Only sampled-then-dropped images counted as a
possible missed host, so reading every found layout's host reported
safe fuses as not_observed for the whole selection.

In limitNativeSample, treat an incomplete inventory as a possible
missed host outside the found layouts unless the read root is itself
a found layout, where host identity already settles coverage.

* fix(audit): apply script targets to their exact source path

A page's script element was recorded only by its NFC lookup key, so on
a normalization-sensitive filesystem a distinct file spelled in another
normalization form inherited the JavaScript role. An unrelated linker
map such as café.map beside a loaded café.map was never classified
and debug companion coverage could settle as not observed.

Keep the resolved spellings each page wrote under the lookup key, as map
declarations already do, and give the script role only to a member or
sibling whose exact path a page loaded.

* fix(audit): keep partial inventory uncertain beside a root ASAR layout

When the read root was itself a found ASAR layout, an incomplete
inventory was trusted to have listed every possible fuse host. A second
asar: false build under a subdirectory names no layout and sits outside
the root layout, so an inventory that stopped after the root host could
miss its launcher and still report the root's safe fuses as
not_observed. The same launcher, once listed, already kept the result
unknown.

Drop the root-layout exemption in limitNativeSample so any incomplete
inventory beside a found layout keeps fuse-host coverage unknown. A
complete inventory that lists only the named host still settles.

* fix(audit): read the linker's --just-symbols file operand

A Go build that forwarded -Wl,--just-symbols=private.syms through
-extldflags or CGO_LDFLAGS kept the relative symbol file in its build
settings, but the forwarded linker matcher knew only the short -R
spelling. Complete metadata then reported go.build_paths as
not_observed while the equivalent -R build was exposed.

Add --just-symbols to the linker options that read a file. Only -R
turns into -rpath when given a directory, so the $ORIGIN ambiguity
check stays on the short spelling.

* fix(audit): anchor expanded package members to the selected path

An inventory-expanded package member reports its location relative to
the selected path, such as app.asar/boot.map, but its lookup identity
resolved that location against the process working directory. An
on-disk page loading app.asar/boot.map, or an absolute file: map
declaration into the archive, resolved under the selected root, so the
declaration never matched the member it named. A script target with an
inline map then settled as a linker map, and a declared map stayed
skipped.

Resolve every member lookup and declaration base against the selected
path, and keep the reported location as written. Linker-map retraction
compares the same anchored spelling.

* fix(audit): read GCC's -I- include barrier as an option

CGO_CFLAGS=-I- is GCC's include barrier, the same as --include-barrier,
not -I with the directory "-". The compiler path matcher read it as a
build path, so a Go binary retaining it reported go.build_paths exposed.
Skip that exact field before path matching.

* fix(audit): keep packaged-image uncertainty after reading the host

A native image stored inside a package is never opened by a fuse reader,
so reading the Electron host on disk does not settle its fuses. Count the
packaged omission toward a partial fuse sample on its own, so safe host
implications stay unknown instead of not_observed while a packaged image
is unread.

* fix(audit): require every named host in a layout to be sampled

An ASAR layout can name more than one host, such as `a/a.exe` beside
`a/electron.exe`. The sample counted the layout as read when any one
named host was kept, so dropping the other still reported the Electron
executable as included and let safe fuses from the sampled images settle
run_as_node as not_observed. Count a layout as read only when every host
it could have was sampled, reusing the named-host selection the fuse
reducer already applies.

* fix(audit): keep a layout-less bundle read from settling partial inventory

A bundle with no ASAR layout, such as an asar: false build, counted its
framework read as covering the Electron host even when inventory stopped
early or the sample dropped an image. A nested build's host could sit in
the part never listed, so the root framework settled RunAsNode as
not_observed. Apply the same incomplete-inventory guard the layout branch
uses to the bundle read.

* fix(audit): read the linker's --sysroot directory operand

A Go ELF build that forwarded -Wl,--sysroot=private-sdk through
-extldflags or CGO_LDFLAGS kept the relative sysroot in its build
settings, but the forwarded linker matcher did not know the option.
Complete metadata then reported go.build_paths as not_observed, while
the compiler-owned --sysroot was already recognized.

Add --sysroot to the linker options that take a path, so the -Wl comma
form, the -Wl = form and both -Xlinker forms are read.

* test(audit): group Deno boundary regressions

* fix(audit): preserve Go linker file operands

* fix(audit): track script completion per source occurrence

* refactor(audit): give Go path parsing one semantic owner

* refactor(audit): preserve map roles at the parsing boundary

* refactor(audit): consume toolchain operands once

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-11 09:57:55 -05:00
morlutoandCursor Agent a6e02657e6 fix(artifacts): preserve unknown relationship coverage (#9904)
* fix(artifacts): preserve unknown relationship coverage

* fix(artifacts): retain producer relationship availability

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-10 22:52:59 -05:00
22f28644fe feat(artifacts): inventory and extract Deno compile executables (#8527)
* feat(artifacts): inventory and extract Deno compile executables

Adds Deno standalone artifact support for Mach-O, ELF, and PE carriers,
exposing virtual files, entry points, recorded options, and file digests
without running the executable. Extraction uses the existing safe output
tree, preserves symlink targets as metadata, and assigns indexed paths
to unsafe or oversized names. Environment-file values and certificate
bytes remain in local Evidence; npm snapshots are reported by length
without decoding. The documentation describes supported layouts, parsing
limits, and refusal behavior.

* fix(artifacts): resolve nested Intel Deno carriers to the enclosing app

The backward trailer search stopped at the first confirmed sentinel, so an
Intel app storing another Intel Deno executable as a virtual file reported
the inner app. Search to the slice start, drop sentinels inside a confirmed
payload, and fail closed on side-by-side trailers that differ.

Correct the docs on retained environment values and generated regions.

* fix(artifacts): keep Deno coverage, remote paths and PE resources truthful

Report unknown coverage when the virtual filesystem cannot be listed, keep
resolved specifiers when another remote ID is missing, and percent-encode
characters in remote paths that a Windows destination would refuse. Reject a
directory at the PE resource language level, and correct the guide's
description of colliding generated regions.

* fix(artifacts): normalize Deno remote escapes so hex case cannot collide

Uppercase every percent escape in a generated remote path, so specifiers that
differ only in escape case reach the path registry as one name and the second
gets an indexed path instead of colliding on a case-insensitive disk.

* fix(artifacts): fail closed on truncated enclosing Deno trailers and encode reserved remote path characters

The Intel trailer scan rejected an overlong sentinel before reading its
marker, so a truncated enclosing app vanished and the nested app was
reported. Read the marker first and fail closed when a confirmed overlong
trailer encloses another trailer.

encodeURIComponent leaves `*` alone and the sanitizer covered only part of
the Windows reserved set. remoteDestination now encodes the whole set once
as uppercase percent hex; source_path keeps the original.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuWHGvzJkrhCqjtEHUwqyH

* fix(artifacts): defer truncated Intel Deno trailer failure until enclosing trailers resolve

A damaged Intel asset stored before a valid one inside a valid app made the
backward trailer search fail before it reached the outer trailer. Hold that
failure until the search ends and drop it when a confirmed enclosing trailer
holds the sentinel as data.

* fix(pe): fail closed when a resource type branch is unreadable beside a found name

namedResourceTargets skipped a type branch whose names directory, or a
name entry whose string, could not be read. A duplicate RT_RCDATA entry
pointing out of bounds therefore vanished once another branch matched,
and locatePeResourcePayloads returned only the readable ranges while
parsePeResources rejected the same tree.

The walk now records unresolved branches. hasPeResourceName still
reports false for a tree that never reaches the name, but once
locatePeResourcePayloads finds the name it raises a format failure if
any branch was left unresolved.

* fix(artifacts): skip Intel Deno sentinels that a native segment maps

A non-Deno x86_64 Mach-O that stores an Intel Deno executable in __DATA
had the asset's trailer promoted to the root application, because the
backward sentinel search never checked who owned the bytes it scanned.

locateMachOSlice now records the file ranges of every segment other than
__SUI and __LINKEDIT (libsui extends __LINKEDIT over the trailer), and
the Intel search skips any sentinel inside one of them. A slice whose
load commands do not frame stays with native inventory, since ownership
cannot be shown there.

* fix(artifacts): reject a universal Deno image with an unreadable sibling slice

The universal Mach-O locator skipped any slice whose load commands could
not be read or framed, and any slice with a FAT range issue. When another
slice confirmed Deno, that slice was reported as the whole image, so a
sibling declaring a different Deno application went unexamined.

locateMachOSlice now reports why part of a slice was not examined
(unreadable or unframed load commands, a truncated slice, or the Intel
sentinel allowance). locateMachO collects those reasons for every
declared and hidden slice and fails closed once any slice confirms a
Deno section. With none confirmed, the image stays with native inventory
as before.

* fix(scripts): supervise the Deno verifier version check

Run `deno --version` through the verifier command owner with a deadline
instead of raw execFile, and close the owner when the version is refused.

* fix(artifacts): keep unconfirmed Deno carriers native and fail truncated Intel trailers

An ELF SUI note whose descriptor is too short for the full d3n0l4nd name
now stays unconfirmed instead of failing closed; failures after the name
matches still fail closed.

Every marker-confirmed Intel trailer whose payload runs past the slice is
now kept until the backward search ends and fails unless an enclosing
payload contains it, so an Intel Deno app missing its last byte reports a
Deno failure instead of native inventory.

The Deno MCP boundary test now calls open_binary and inspect_artifact
through artifactMcpCall, as docs/testing.md requires.

* test(deno): drop source-text checks from verifier pin test

Keep the wrong-version subprocess check, which covers the pinned
release through behavior, and stop asserting on the verifier script's
source spelling.

* test(deno): close readers in finally in symlink and cancellation tests

A failed assertion or read error in either test skipped reader.close(),
leaving the file open while temp-directory cleanup ran. Both tests now
close the reader in finally, as CONTRIBUTING.md requires. The
closed-reader assertion in the symlink test stays after the finally.

* docs(specs): add deno-standalone spec, plan and tasks

Add docs/specs/artifacts/deno-standalone/ with spec.md, plan.md and
tasks.md for the Deno compile executable reader, as AGENTS.md requires
for a feature, and list them in the specifications index.

* docs: list the Deno compile guide in the documentation sidebar

* fix(deno): confirm the d3n0l4nd section before claiming Mach-O damage

A __SUI segment alone no longer makes later load-command damage Deno's.
A Mach-O whose __SUI segment holds only a differently named section now
stays absent and keeps native inventory, while damage seen before or
beside a confirmed d3n0l4nd section still fails closed.

* fix(deno): keep a slice with an unparsed segment command unread

A Mach-O segment command that frames but cannot be parsed may be the
__SUI segment, so the slice is no longer treated as fully examined. A
universal image whose other slice confirms Deno now fails closed instead
of letting that sibling supply the whole inventory, and with no confirmed
sibling the image still keeps native inventory.

* fix(deno): confirm an ELF SUI note before rejecting its length

A SUI note whose descsz runs past its note table was recorded as plain
damage before its descriptor name was read, so a malformed Deno carrier
fell back to native inventory. The owner and the d3n0l4nd name prefix are
now read within the table apart from the claimed extent, and a confirmed
note with an oversized extent fails closed.

* fix(deno): confirm a truncated __SUI section record by its name

A section record cut short after its 16-byte name was skipped, so a
damaged Deno carrier fell back to native inventory. The name is now read
apart from the rest of the record, and a truncated record that names
d3n0l4nd fails closed. The range checks for a confirmed record move into
their own helper.

* fix(deno): reject nonzero bytes after the last ELF note

When fewer than 12 bytes remained in a note table after its last note,
the walk stopped without looking at them, so a Deno note was selected
with complete coverage beside unaccounted data. The remainder is now
read: zero padding is accepted, and nonzero bytes are recorded as damage,
which fails closed once a SUI note is confirmed and otherwise leaves the
ELF to native inventory, matching the native note parser.

* fix(deno): validate FAT slice identity before choosing the search

The locator took a universal slice's CPU type from its FAT row, so a row
that called an x86_64 slice arm64 skipped that slice's Intel trailer and
let the arm64 payload stand for the whole image. Each slice's header is
now checked against its row with validateFatSliceIdentity. A mismatch
leaves the slice unread, so a universal image whose other slice confirms
Deno fails closed. The Intel fixture header now states
CPU_SUBTYPE_X86_64_ALL so its FAT rows can match it exactly.

* fix(deno): check the Intel trailer marker before judging its length

A trailer length word of 2^53 or more was dropped before its d3n0l4nd
marker was read, so an enclosing app with that length was skipped and a
nested app's range stood for the whole image. The marker is now read
first, and a marker-confirmed trailer whose length runs past the slice,
safe or not, joins the deferred truncated candidates and fails closed
unless an enclosing trailer holds it as data.

* fix(deno): count short ELF note tables against the note allowance

A note table shorter than one note header made a read of its trailing
bytes but counted no note, so many distinct tiny tables could force one
sequential read each past the 256-note allowance. Every nonempty table
now counts at least one entry, both in the up-front estimate and while
scanning, so an image past the allowance records the limit instead of
reading every table. The trailing-byte check moves into its own helper.

* fix(deno): fail closed on ELF extended section numbering beside a SUI note

A zero e_shnum with a nonzero e_shoff moves the section count into
section zero, so the section table was skipped and its SHT_NOTE tables
were never compared. Record it as damage, as PN_XNUM already is, so a
confirmed SUI note fails closed instead of reporting full coverage.

* fix(deno): keep undersized Intel trailers until enclosing ownership is known

A trailer length word below 8 was dropped before its d3n0l4nd marker was
read, so an enclosing app with a length of 0 to 7 was skipped and a nested
app's trailer was selected as the root. The marker is now read first, and a
marker-confirmed trailer too short to hold it is kept as malformed and fails
closed unless an enclosing trailer holds it as data.

* fix(pe): treat non-directory RT_RCDATA branches as unresolved

A duplicate typeId root entry whose target is a data entry was skipped
without marking the walk unresolved, so locatePeResourcePayloads returned
the readable named resource as complete. Route non-directory type
branches through the same unresolved path as unreadable names
directories so the Deno reader fails closed.

* fix(deno): fail closed on a zero-fill d3n0l4nd Mach-O section

denoSectionRange admitted the declared offset and size of a confirmed
__SUI,d3n0l4nd record without checking its section type, so a
S_ZEROFILL, S_GB_ZEROFILL or S_THREAD_LOCAL_ZEROFILL section could be
read as a file payload while MachoImage reports it as not file backed.
Reject zero-fill types with the shared isMachoZeroFillSection predicate
before assigning a physical range.

* fix(deno): bound the d3n0l4nd Mach-O section by its segment file extent

denoSectionRange checked a confirmed __SUI,d3n0l4nd record only against
the slice and file bounds, so a section reaching past its segment's
fileoff/filesize was still read as a complete payload while MachoImage
reports the same section as not file backed. Reject a section outside
its enclosing segment's file extent before admitting the physical range.

* style(deno): apply oxfmt to the DenoStandaloneLocation embedding union

* fix(artifacts): preserve Deno carrier ownership

* fix(artifacts): admit Deno parent segments

* fix(deno): preserve native segment ownership

* test(deno): cover mismatched Mach-O segment widths

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 21:43:01 +08:00
Entrpiandmorluto b43c329df7 feat(artifacts): inventory and extract UF2 and DfuSe firmware containers (#8230)
* feat(artifacts): inventory and extract UF2 and DfuSe regions

open_binary refused UF2 and DfuSe firmware containers as an unsupported
format, so inspect_artifact and extract_artifact could not show which
bytes a drag-and-drop or DFU update writes at which addresses.

UF2 is recognized by a complete first block with all three magic
numbers. Valid main-flash payloads are joined into address-contiguous
regions per familyID, named family-0x<id>/0x<start>-0x<end>.bin, and
known IDs are named from a small table copied from Microsoft's
uf2families.json at a pinned commit. Numbering is counted per familyID
and block count: absent, repeated, out-of-order and out-of-range block
numbers are reported, and absent blocks leave coverage unknown.
Not-main-flash blocks are excluded and reported; file-container blocks
are reported and leave coverage unknown; invalid blocks and a partial
final block are excluded with their position. A rewrite of one address
with different bytes fails closed, naming both block spans.

DfuSe is recognized only by its prefix together with a DFU 1.1a suffix.
Each element becomes one region at its file range with its target's
alternate setting and name. Structural faults fail closed with a byte
offset; overlapping elements stay separate and are reported. A suffix
CRC mismatch follows integrity_policy: fail refuses with a dfu-crc32
integrity error, and record-and-continue records a root contradiction
that extraction also carries.

HEX, UF2 and DfuSe share one retained-region reader and one run
merger, so the HEX decoder and reader lose their private copies.

* docs(artifacts): document UF2 and DfuSe images and their esptool lane

Describe recognition, region names, metadata, numbering, failures, the
DfuSe suffix CRC under integrity_policy, and limits, and link the page
from the CLI and firmware guides and the agent skill reference.

npm run verify:uf2 runs esptool merge-bin --format uf2 on generated
inputs at known addresses, with and without MD5, with 256-byte
payloads, and on a concatenation of two chips' outputs. Inspection and
extraction through the built CLI must return every input at its address
under the familyID esptool itself reports.

* fix(artifacts): accept both DfuSe image size conventions

UM0391 calls DFUImageSize the file length, and dfu-util's dfuse-pack.py
writes that value, while ST's DfuFileMgr, MicroPython's dfu.py and pydfu
leave the 16-byte suffix out. The decoder accepted only the second form,
so it refused dfuse-pack.py output as malformed.

Accept either value and keep the declared one in image_size; any other
value still fails with the suffix offset and the file length. The tests
pin both producers' output for one image, CRC included, from their
build() functions transcribed and run under Python 3.

* fix(artifacts): keep reused UF2 block numbers from hiding missing blocks

esptool numbers every merged input from 0, so two inputs with the same
block count share one numbering group. A block lost from one input left
its number present in the other, and the file reported complete
coverage. Each block number now records the target addresses it was
seen at: equal address counts are complete copies, a resend to the same
address is one block, and unequal counts make coverage unknown with a
limitation and ambiguous_numbering_group_count. sequence_count becomes
numbering_group_count, and region block positions become
first_file_block and last_file_block.

Review follow-ups in the same change:
- compare_artifacts compares a DfuSe element's alternate setting and
  target name, so bytes moved to another device memory are changed;
- the shared reader keeps no region bytes outside its map after close;
- one domain type replaces four identical decoder failure types;
- the UF2 decoder returns a typed limit instead of throwing on bytes
  beyond its admitted size, and caps retained regions, not runs;
- DfuSe caps elements with bytes only, names empty elements and
  element-less targets, drops the constant bVersion and bcdDFU fields,
  and says whether the CRC matches when DFUImageSize fits neither
  convention;
- classification evidence carries a suffix only when the probe saw the
  end, and root classification reads the tail only for a DfuSe prefix;
- the UF2 domain region owns family_name, and the root records the
  family registry revision; the table's MIT notice is preserved;
- docs pin the producer revisions (dfu-util ee6b393d and f9a537c9,
  MicroPython cb99ca98), give the MD5 structure's real meaning, and
  describe container contradictions in the MCP contract guide;
- tests and the esptool lane add the equal-count inputs and a block
  lost from one of them, accepted boundaries beside each refusal, a
  DfuSe root larger than the classification prefix, and CLI refusals.

* feat(artifacts): name the UF2 families that LibreTiny builds for

LibreTiny writes UF2 for Realtek, Beken and Bouffalo Lab modules. The
Microsoft registry revision REA already cites (cf3bd7f9) lists eight of
those families: RTL8710A, RTL8710B, RTL8720C, RTL8720D, BK7231U, BK7231N,
BK7251 and BL602. Their regions now carry those names. LN882H, which
that revision omits, keeps its familyID with no name.

* fix(artifacts): count DfuSe declarations without bytes instead of listing them

Empty DfuSe elements have no entry, so DFUSE_MAX_ELEMENTS does not cap
them, and the target walk kept a description string for each one. At 8
bytes per element, a file under the 128 MiB ceiling can declare about
16.7 million of them. The walk now counts every declaration without bytes
and builds only the first NAMED_ELEMENTS descriptions, which is all that
the limitation shows. The limitation text and empty_element_count do not
change. The guide now says that both DfuSe limitations name the first four.

On this host, decoding 4,000,000 empty elements (a 32 MB file) ran out of
memory under a 128 MB V8 heap before this change, and it peaked at 1.59 GB
RSS with the default heap. It now decodes in 114 ms with a 220 MB peak.

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-31 16:27:00 +08:00
22e5aa97af feat(artifacts): inspect classic iNES container regions (#2816)
* feat(artifacts): inspect classic iNES container regions

* fix(artifacts): preserve iNES source disappearance as integrity failure

* fix(artifacts): revalidate iNES source before parsing header

* fix(artifacts): validate admitted roots before classification

* fix(artifacts): normalize source loss during root hashing

* fix(artifacts): bind iNES extraction admission to inventory

* test(artifacts): clarify non-executing inspection wording

* fix: preserve binary plist detection with extended magic probe

* test(browser): wait for popup document loads before capture

* fix(artifacts): honor cancellation across source validation

* fix(artifacts): prefer artifact magic over implicit database suffix

* fix(artifacts): select extraction readers from scanned format

* fix(artifacts): complete prefix reads and share ZIP format admission

* fix(browser): budget scenario cleanup for content drain and shutdown

* test(artifacts): admit valid universal inputs before routing

* test(artifacts): verify iNES workflows on macOS

* test(ci): own iNES macOS coverage and portable fixture paths

* fix: preserve cleanup retries and avoid duplicate inventory parsing

* fix(contracts): share ZIP target formats across MCP lifecycle schemas

* fix(target): preserve typed cancellation during descriptor admission

* refactor(artifacts): use JSON-safe inventory producer output directly

* test(artifacts): share file read setup and satisfy CI checks

* fix(artifacts): classify changed admission as integrity failure

* ci: run Mach-O summary delivery coverage on macOS

* test(javascript): align parameter provenance with main contract

* test(browser): await discoverable fixture pages before runtime verification

* fix(artifacts): pass target metadata to format detection

* fix(artifacts): bind extraction to opened target

* fix: repair the static-check failures on the default branch (#2895)

* fix: repair the static-check failures on the default branch

The default branch fails its own required `Static checks` job three ways, so
every pull request reports that job as failed regardless of its own change.

- `npm run format:check` reports `src/domain/functionComparison.test.ts` and
  `src/ida/IdaOperationRunner.ts`.
- `npm run lint` reports a duplicate `import { join } from "node:path"` in
  `tests/boundary/mcp/ghidraInventoryMcp.test.ts` (lines 4 and 17).
- `npm run typecheck` reports `format: "devin"` missing from the client format
  union in `src/application/SupportedClients.ts`, and an unchecked index in
  `tests/boundary/cli/clientProfileLifecycle.test.ts`, whose pair array is
  inferred as `string[][]`.

Apply the pinned formatter, drop the duplicate import, list the `"devin"` format
that its own definition already uses, and mark the pair array `as const`.

Validation: `npm run check:fast` and `npm run format:check` pass.

* fix(setup): keep Devin on the JSON configuration dialect

---------

Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
(cherry picked from commit df3d96da80)

* fix(artifacts): bind session inventories to target identity

* test(browser): bound fixture readiness acknowledgements

---------

Co-authored-by: 64579525 <17373302683@163.com>
Co-authored-by: 吴佳浩Alben <35291372+XiaoBinGan@users.noreply.github.com>
Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
2026-10-31 15:49:16 +08:00
8188497f13 feat(artifacts): inspect and extract Bun standalone module graphs (#6998)
* feat(artifacts): inventory and extract Bun standalone executable module graphs

Adds a Bun standalone artifact reader for Mach-O, PE, ELF and appended
trailers, supporting the 36-byte and 52-byte module record layouts. It
preserves module names, entry-point details and raw metadata, and
exposes contents, sourcemaps, bytecode and module-info as separate
entries for safe extraction without executing the binary. Parsing
rejects malformed or ambiguous graphs and enforces module-count,
retained-byte and expansion limits. Adds synthetic parser tests, CLI and
MCP coverage, agent skill guidance, and a verification script for
Bun-produced executables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(artifacts): bound Bun section probes and reject truncated sections

Split Bun recognition into a bounded probe that reads only section
headers, length prefixes, and the Offsets/trailer suffix, and a single
payload load used by the reader. Inventory classification and extraction
admission no longer allocate the whole graph.

Match ELF `.bun` section names by comparing only the name bytes and
terminator, so many headers sharing one long name cost constant work each.
Reject sections whose declared extent ends past EOF instead of reading a
silently shortened window.

Launch Bun, Docker, and REA in the standalone verifier through the shared
owned verifier commands, and release the runtime root only after those
command families have settled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* style(artifacts): format Bun reader integration

* fix(contracts): describe static Bun inventory facts

* fix(artifacts): bound Bun metadata reads and combined path growth

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-29 22:31:50 +08:00
blankymandgensokyo-dev e78ac1a55b feat(artifacts): read UnityFS AssetBundle containers (#7699)
* feat(artifacts): read UnityFS AssetBundle containers

inspect-artifact and extract-artifact now recognize the UnityFS signature
and treat format versions 6 through 9 as containers. Header facts (format
version, player/engine strings, declared size, dataflags, blocks-info
bounds and the declared 16-byte data hash) are retained alongside a
directory of CAB-* and .resS members. Member bytes stream on demand:
only the blocks covering the requested member are read and decompressed
with bounded expansion and cancellation.

Bundles declaring asset-bundle encryption keep header facts and report
unknown directory coverage. LZMA blocks-info reports unknown coverage;
LZMA member reads fail as unavailable while the directory stays
readable. Malformed counts, out-of-stream members and declared sizes
beyond the file fail as format errors. UnityFS classifies by bytes in
root detection, inventory graph construction and desktop inspection,
which emits a unityfs-header fact.

Validation: tsc --noEmit, oxlint (no new findings on touched files),
17 reader tests covering LZ4HC/uncompressed tables, blocks-info at end,
format 6 without padding alignment, multi-block members, malformed
input, unsupported compression, cancellation and abandoned streams;
full artifacts/domain/conformance suites pass. Verified against one
real YooAsset bundle (Unity 2021.3.32f1, format 8, LZ4HC): the
streamed SerializedFile contained its managed assembly byte-identical
to a UnityPy extraction. Other producer variants unverified.

* fix(unityfs): own bounded member reads and preserve partial evidence

* refactor(unityfs): clarify producer revision flag decisions

* fix(artifacts): preserve coverage authority and bound UnityFS block work

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-28 16:47:39 +08:00
morluto 39179db68d refactor(artifacts): unify inventory integrity policy 2026-10-27 09:05:18 +08:00
morluto 60834e391c fix(artifacts)!: compare ASAR storage declarations
Record packed and unpacked ASAR file declarations at the reader boundary and compare known storage changes without treating historical omissions as packed. Preserve known differences when bytes are unavailable and retain existing integrity precedence.

BREAKING CHANGE: ASAR file occurrences may now contain unpacked: false. Consumers that validate only true must accept an optional boolean; absent historical fields remain unknown.
2026-10-27 05:59:40 +08:00
ed79bf9361 feat(artifacts): inventory and extract Node.js single executable application blobs (#7000)
* feat(artifacts): inventory and extract Node.js single executable application blobs

Adds a node-sea artifact reader that locates the NODE_SEA_BLOB payload in
Mach-O sections, ELF notes and PE RCDATA resources, decodes the blob
header, main script, optional snapshot and code cache, and named assets,
and extracts their stored bytes through the existing safe output tree.
The executable is never run. Resource-tree errors in a PE without a
NODE_SEA_BLOB name stay with native classification; truncated, unknown
or over-limit SEA declarations fail closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(artifacts): reuse PE resource facts and bound SEA asset tables

Move the bounded NODE_SEA_BLOB name probe into the shared PE resource
layout module so SEA admission uses the same header and section mapping
as the full parser; the layout reader now accepts a header prefix with
the file size. Bound SEA asset count and decoded name bytes before
records are created and report resource bounds as limit failures. Offer
the JavaScript next probe only for main scripts with a source suffix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(artifacts): make SEA metadata construction cancellable

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-25 02:10:41 +08:00
morluto 693ecec082 fix(artifacts): retain permission omissions through JavaScript lookups 2026-10-24 03:10:43 +08:00
morluto 5e4004c413 fix(artifacts): preserve traversal coverage through bundle workflows
Retain producer traversal uncertainty through inventory, extraction, Interface Builder decoding and executable-root discovery. Keep known copied files and decoded facts with honest coverage; share omission normalization and bound omitted archive paths with their source reasons.

Fixes #6852
Fixes #6825
Fixes #6582
2026-10-24 02:00:09 +08:00
morluto bd4f5e9c19 fix(parsers): preserve format evidence and caller input identity
Retain bounded FAT and offset-tagged PE observations through archive hashing,
classification, and content nodes. Preserve PyInstaller member paths according
to the observed carrier dialect and share APK v1 signing-name semantics.

Read SQLite's implicit sqlite_schema table without inventing a stored schema
declaration. Distinguish unsupported ELF numbering and decoded-name admission
from malformed data and fixed scan allowances. Preserve Sigrok basenames and
classify matching explicit input-format refusals at their owning boundary.

Validation: 554 distinct tests passed, 3 skipped across serial focused runs;
build, typecheck/lint, docs, formatting, and discovery (2141 files) passed.
Sigrok command-boundary simulators do not establish real-tool decoding.
Small repeated public PyInstaller CLI runs had equivalent inventory facts;
warm median elapsed 0.5431s before / 0.5426s after, with no speed claim.

Fixes #6733
Fixes #6741
Fixes #6722
Fixes #6732
Fixes #6736
Fixes #6734
Fixes #6764
Fixes #6766
Refs #6739
2026-10-23 22:54:28 +08:00
morluto 5bcf71c660 fix(pyinstaller): retain colliding archive observations
Keep source-distinct CArchive rows and decoded PYZ children in inventory. Refuse only when extraction reaches a destination conflict.

Fixes #6674
2026-10-23 21:28:10 +08:00
Kaoru0822andClaude Opus 5.5 8451b961ad feat(artifacts): make ASAR header admission configurable (#6209)
* feat(artifacts): make ASAR header admission configurable

ASAR header/index admission was a fixed 16 MiB with no override, so
operators with large Electron indexes could only extract the archive.

REA_ASAR_MAX_HEADER_BYTES now sets it (default 16777216, maximum
268435456). One owner resolves the effective admission for the
classification probe, inventory, ASAR reader and Electron file scope,
including the Electron header cache budget. Startup and doctor validate
the setting; admission refuses an invalid value instead of falling back.
Recovery and error details name the effective value and its source.

Fixes #6176

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(artifacts): resolve ASAR header admission from the caller's environment

The admission read process.env, so composition roots that select their own
environment (createDirectAnalysis, createServer, CLI, optional observation
factories) could not raise or lower it, and an ambient value leaked into
callers that left it unset.

Nothing reads process.env now. ArtifactProvider and extraction resolve the
admission from their environment; JavaScript analysis parses it from the
environment its server or CLI caller passes and refuses an invalid value;
the Electron and V8 providers receive it from their factories. Inventory,
nested ASAR readers, classification probes and Electron page authorization
carry that one value. A header cached by Electron under a larger admission
is refused for a smaller one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:18:22 -07:00
Matt Van Horn afa5da745e feat(artifacts): inspect and extract PyInstaller archives 2026-10-21 12:48:29 +08:00
5fa4defcbc feat(artifacts): inspect and extract Godot PCK with retained facts (#3634)
* feat(artifacts): read Godot PCK packs

Add a Godot Engine PCK artifact reader for standalone packs and packs
embedded behind an executable trailer (formats 2 and 3). Members are
uncompressed byte ranges verified against their declared MD5 on read.

Encrypted directories, sparse bundles, patch removal entries, unknown
flags and format 1 fail closed as unavailable; encrypted members are
reported without being read. inspect-artifact and extract-artifact
recognize standalone packs by their GDPC magic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(artifacts): bound Godot PCK metadata and own member streams

Cap member paths at 4096 bytes and the retained directory at 64 MiB
before allocation. Read members with positional chunk reads through
pipeline so cancellation is typed, abandoned consumers release their
source, and the shared descriptor stays reader-owned. Retain the root
descriptor for godot-pck inventory, as for ZIP and universal Mach-O.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(artifacts): complete Godot PCK facts, integrity and selected views

* fix(artifacts): preserve atomic-root inventory Evidence

* fix(artifacts): bound aggregate PCK member expansion

---------

Co-authored-by: Samucahub <151155051+Samucahub@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-20 09:53:53 +08:00
morluto e8da7e1edd fix(artifacts): share bounded ASAR signature observations
Parse Pickle extents once at the ASAR boundary and carry finalized signature
facts through root and nested classification. Scan JSON whitespace within its
declared extent and decode budget without retaining the whitespace prefix.
Keep prefix storage bounded independently of stream chunk ownership.

Fill fixed ASAR prefixes and DMG trailers across short reads, preserve
cancellation through admission and stream EOF, and retain existing cleanup
ownership. Cover public CLI/MCP selection, root/nested inventory, malformed
JSON whitespace and padding, limits, native/XML consumers and descriptor faults.

Validation: 24 focused files / 170 tests; full check; docs check; test discovery.
Official inert ASAR fixtures and controlled descriptor faults were exercised;
real Electron execution, Windows and natural OS short-read frequency remain
unverified. Equivalent compact CLI tasks returned identical results and reply
token estimates; repeated timings do not establish a speedup.

Fixes #3574
Fixes #5188
2026-10-19 14:32:48 +08:00
7777abf473 fix(artifacts): preserve ZIP member identity and filename semantics
Retain duplicate archive occurrences with validated ordinal identities across pages; qualify ambiguous comparison, parent and source-map relationships instead of overwriting entries. Preserve exact integrity-contradiction ownership. Decode unflagged names as CP437 while honoring UTF-8 and valid Unicode Path overrides.

Fixes #3152

Fixes #3237

Co-authored-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>

Co-authored-by: gongfpp <1360187968@qq.com>
2026-10-14 15:20:09 +08:00
45a3509db3 fix(artifacts): compare inventories using declared coverage (#2214)
* fix(artifacts): preserve complete root inventory comparisons

* fix(artifacts): declare inventory coverage before comparison

* fix(artifacts): preserve traversal omissions and materialization limits

* fix(artifacts): preserve coverage declarations and projection scope

Remove the duplicate inventory-schema import. Compare original coverage declarations, including field omission, before adapting legacy pages. Require explicit scope for completeness and container-entry coverage for application projections.

Source inspection and git diff --check only. Tests, builds and typechecks skipped by explicit user request. GitHub review writes paused globally.

* fix(artifacts): require entry coverage for child changes

* fix(artifacts): accept source-valid inventory limitations

* style(artifacts): format mobile graph projection

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: OpenAI Codex <openai-codex@users.noreply.github.com>
2026-10-14 03:00:27 +08:00
morluto a17e257f43 fix(artifacts): preserve failures while unwinding nested traversals
Finish every remaining iterator from inner to outer, retaining cleanup observations without replacing the original traversal failure. A real nested ASAR regression reproduces the previous integrity-error masking.
2026-10-11 05:33:14 +08:00
morluto a147ecd4b4 perf(artifacts): index observed parents for deep archive paths 2026-10-11 02:10:50 +08:00
Kaoru0822andClaude Opus 5.5 4b985e5021 fix(artifacts): take nested container families from bytes (#1553)
Inventory gave nested members a ZIP or ASAR family from their path
suffix. Byte classification replaced it only for ZIP magic, so text named
`.apk` or `.asar` stayed a container, and encrypted entries and symlinks
kept the suffix guess. A filesystem `.asar` without an ASAR header failed
the whole inventory as a malformed archive.

Nested ZIP families now follow ZIP magic and ASAR follows its header
pickle, shared with root classification. An unread member named like a
container has an unknown format and a limitation naming the suffix. ASAR
expansion requires the header.

The Android projection adds a limitation when no root AndroidManifest.xml
was observed, and its rejection names the observed root format.

Fixes #1491

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:17:40 -07:00
morlutoandCursor Agent 1f0775684b fix(artifacts): keep case-distinct archive names in inventory (#1480)
ZIP member names are case-sensitive. Inventory was folding them with a
filesystem case rule, so shrunk APKs never became evidence for the Android
application graph. Logical registration now keeps both spellings and records
an English Unicode case-folding limitation. Extraction fails only when the
destination directory already holds the other spelling.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-10 15:21:16 +08:00
morluto d79317cda0 fix(artifacts): bind ASAR interpretation to owned snapshots
Verify captured container bytes against their inventoried digest before listing or reading packed members. Isolate per-reader ASAR metadata, preserve original unpacked companions, and retain complete inventories when snapshot cleanup fails.
2026-10-10 06:20:28 +08:00
morluto 63bf91405c refactor!: enforce canonical evidence and provider boundaries
Deliver complete Evidence directly through MCP with required nullable analysis profiles and immutable per-server delivery settings. Bind selected environments at construction and require typed cleanup outcomes with retained retry ownership.

Remove test-only routing, duplicate session and snapshot metadata, producer-field repairs, and ambiguous provider arguments. Separate artifact byte identity from occurrence classification and retain typed source-map and partial native facts across CLI, MCP, and verifier consumers.

BREAKING CHANGE: obsolete Evidence envelopes, session aliases, incomplete snapshots, and missing normalized producer facts are no longer accepted.
2026-10-09 20:18:52 +08:00
Ysopwnable 6e4535c101 refactor(artifacts): move inventory and extraction into provider owners (#1032) 2026-10-08 05:51:14 +08:00