* feat(native): read embedded cargo-auditable Rust dependency lists * fix(native): trim PE section padding and compute dependency cycles iteratively * fix(native): ignore SHT_NOBITS sections and validate dependency edges linearly * fix(native): bound compiler path scanning and retained hashes * fix(rust): pass package arrays as named parameterized test cases * fix(rust): admit graph expansion and preserve resource units * fix(rust): reject malformed packages, false cycles, and hidden compiler paths Fail the first malformed cargo-auditable package before Zod collects an issue per field. Require cyclic dependency indices to match the cycles in package dependencies. Keep read-only ELF ranges when a section name does not fit the bounded name window, so a rustc path in that range is still reported. * fix(ci): repair integrated checks and Hopper test baseline * fix(go): verify Mach-O sections against segments --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import { isAbsolute } from "node:path";
|
|
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
|
import type { RustBinaryPort } from "./RustBinaryPort.js";
|
|
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
|
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
|
|
import {
|
|
inspectRustBinaryInputSchema,
|
|
rustBinarySchema,
|
|
} from "../../domain/rust/rustBinary.js";
|
|
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
|
|
const OPERATION = "inspect_rust_binary";
|
|
|
|
const architectureForSubject = (architecture: string) => {
|
|
switch (architecture) {
|
|
case "386":
|
|
return "x86";
|
|
case "amd64":
|
|
return "x86_64";
|
|
default:
|
|
return BINARY_ARCHITECTURES.find(
|
|
(supported) => supported === architecture,
|
|
);
|
|
}
|
|
};
|
|
|
|
/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
|
|
export class RustBinaryService {
|
|
constructor(readonly provider: RustBinaryPort) {}
|
|
|
|
/** Inspect one explicitly selected file without acquiring a binary session. */
|
|
async inspect(
|
|
rawInput: unknown,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
if (options?.signal?.aborted)
|
|
return err(new AnalysisCancelledError(OPERATION));
|
|
const input = inspectRustBinaryInputSchema.safeParse(rawInput);
|
|
if (!input.success)
|
|
return err(
|
|
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
|
|
cause: input.error,
|
|
}),
|
|
);
|
|
if (!isAbsolute(input.data.path))
|
|
return err(
|
|
new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: "Expected an absolute filesystem path on this host.",
|
|
},
|
|
]),
|
|
);
|
|
const inspected = await this.provider.inspect(input.data, options);
|
|
if (!inspected.ok) return inspected;
|
|
if (options?.signal?.aborted)
|
|
return err(new AnalysisCancelledError(OPERATION));
|
|
const report = rustBinarySchema.safeParse(inspected.value);
|
|
if (!report.success || report.data.artifact.path !== input.data.path)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
OPERATION,
|
|
"Rust dependency reader returned malformed metadata or changed the selected artifact identity.",
|
|
),
|
|
);
|
|
return ok(evidenceFor(this.provider.identity, input.data, report.data));
|
|
}
|
|
}
|
|
|
|
const evidenceFor = (
|
|
provider: RustBinaryPort["identity"],
|
|
parameters: { readonly path: string },
|
|
value: ReturnType<typeof rustBinarySchema.parse>,
|
|
): Evidence => {
|
|
const architecture = architectureForSubject(value.architecture);
|
|
const section = value.auditable_metadata;
|
|
return createEvidence(
|
|
{
|
|
path: value.artifact.path,
|
|
sha256: value.artifact.sha256,
|
|
format: value.format === "macho" ? "mach-o" : value.format,
|
|
...(architecture === undefined ? {} : { architecture }),
|
|
},
|
|
provider,
|
|
{
|
|
operation: OPERATION,
|
|
parameters,
|
|
result: value,
|
|
confidence: "observed",
|
|
limitations: value.limitations,
|
|
locations: [
|
|
{ kind: "artifact-path", path: value.artifact.path },
|
|
...(section === null
|
|
? []
|
|
: [
|
|
{
|
|
kind: "file-offset-range" as const,
|
|
start: section.file_offset,
|
|
end: section.file_offset + section.section_bytes,
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
);
|
|
};
|