mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 15:56:35 +00:00
fix(javascript): accept the Wakaru 1.x release line and verify 1.14.0 (#1540)
* fix(javascript): accept the Wakaru 1.x release line and verify 1.14.0 Exact --version equality rejected every Wakaru build except 1.13.0, and strict report schemas rejected any added field, so each upstream release broke recovery until REA re-pinned. Wakaru now documents that --version, --json and provenance.json change only by addition within a major version. Admit ^1.13.0 with the verified/compatible split used for Binwalk and Unblob, report the observed version instead of the pinned constant, and add a limitation for unverified releases. Parse reports with loose objects and keep the reported module status as a string; path, identity, range and file-set checks stay exact. Move the audited pin, submodule, CI binary and verifier to 1.14.0, and document where to get the binary from the refusal message. * fix(javascript): project known Wakaru warning fields --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
0363e5a8e1
commit
ded59689ce
16 files changed
+272
-66
No files matched your search
@@ -26,7 +26,7 @@ import { RECOVERY_LIMITS } from "./WakaruRelease.js";
|
||||
|
||||
const OPERATION = "recover_javascript_sources";
|
||||
const WAKARU_REQUIREMENT =
|
||||
"Provide an absolute REA_WAKARU_COMMAND pointing to Wakaru 1.13.0 on Linux x64; no tool is installed by REA";
|
||||
"Provide an absolute REA_WAKARU_COMMAND pointing to a Wakaru ^1.13.0 Linux x64 binary (verified with 1.14.0); no tool is installed by REA. See docs/javascript-recovery.md#install-wakaru.";
|
||||
|
||||
/** Inject the owned process launcher while retaining production protocol parsing. */
|
||||
export type WakaruLauncher = (
|
||||
|
||||
@@ -24,11 +24,8 @@ import {
|
||||
} from "./WakaruCommand.js";
|
||||
import { parseWakaruReports } from "./WakaruReport.js";
|
||||
import { publishWakaruArtifacts } from "./WakaruPublication.js";
|
||||
import {
|
||||
RECOVERY_LIMITS,
|
||||
WAKARU_RELEASE,
|
||||
WAKARU_PROVIDER_IDENTITY,
|
||||
} from "./WakaruRelease.js";
|
||||
import { RECOVERY_LIMITS, WAKARU_PROVIDER_IDENTITY } from "./WakaruRelease.js";
|
||||
import { admitWakaruVersion } from "./WakaruVersion.js";
|
||||
|
||||
const OPERATION = "recover_javascript_sources";
|
||||
const MODES = {
|
||||
@@ -66,12 +63,13 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
|
||||
...commandContext,
|
||||
arguments: ["--version"],
|
||||
});
|
||||
if (
|
||||
version.exit_code !== 0 ||
|
||||
version.stdout.trim() !== `wakaru ${WAKARU_RELEASE.version}`
|
||||
) {
|
||||
const admitted =
|
||||
version.exit_code === 0
|
||||
? admitWakaruVersion(version.stdout.trim())
|
||||
: undefined;
|
||||
if (admitted === undefined || admitted.status === "unsupported") {
|
||||
// The message names the mismatch; the reason also keeps the tool's stderr.
|
||||
const mismatch = `This adapter requires Wakaru ${WAKARU_RELEASE.version}; reported version: ${version.stdout.trim()}; exit: ${String(version.exit_code)}`;
|
||||
const mismatch = `${admitted?.message ?? `Wakaru --version failed; reported version: ${version.stdout.trim()}`}; exit: ${String(version.exit_code)}`;
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"wakaru",
|
||||
OPERATION,
|
||||
@@ -106,20 +104,22 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
|
||||
sourceBytes: snapshot,
|
||||
files,
|
||||
});
|
||||
return { version, run, parsed };
|
||||
return { version, admitted, run, parsed };
|
||||
};
|
||||
|
||||
/** Publish validated recovery and retain exact source and engine bindings. */
|
||||
export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
|
||||
const { root, tree, input, source, engine, options } = context;
|
||||
const staging = join(root, "modules");
|
||||
const { version, run, parsed } = await executeWakaruRecovery(context);
|
||||
const { version, admitted, run, parsed } =
|
||||
await executeWakaruRecovery(context);
|
||||
const result = await publishWakaruArtifacts({
|
||||
tree,
|
||||
staging,
|
||||
input,
|
||||
source,
|
||||
engine,
|
||||
admitted,
|
||||
parsed,
|
||||
execution: run,
|
||||
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
||||
@@ -150,25 +150,29 @@ export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
|
||||
OPERATION,
|
||||
"Configured engine bytes changed during recovery",
|
||||
);
|
||||
return createAnalysisExecution(result, WAKARU_PROVIDER_IDENTITY, {
|
||||
rawResult: jsonValueSchema.parse({
|
||||
version,
|
||||
execution: run,
|
||||
report: parsed.report,
|
||||
provenance: parsed.provenance,
|
||||
}),
|
||||
subject: {
|
||||
path: source.path,
|
||||
sha256: source.sha256,
|
||||
format: "javascript",
|
||||
return createAnalysisExecution(
|
||||
result,
|
||||
{ ...WAKARU_PROVIDER_IDENTITY, version: admitted.version },
|
||||
{
|
||||
rawResult: jsonValueSchema.parse({
|
||||
version,
|
||||
execution: run,
|
||||
report: parsed.report,
|
||||
provenance: parsed.provenance,
|
||||
}),
|
||||
subject: {
|
||||
path: source.path,
|
||||
sha256: source.sha256,
|
||||
format: "javascript",
|
||||
},
|
||||
locations: [
|
||||
{ kind: "artifact-path", path: source.path },
|
||||
...result.modules.map((module) => ({
|
||||
kind: "artifact-path" as const,
|
||||
path: module.artifact.path,
|
||||
})),
|
||||
],
|
||||
limitations: result.limitations,
|
||||
},
|
||||
locations: [
|
||||
{ kind: "artifact-path", path: source.path },
|
||||
...result.modules.map((module) => ({
|
||||
kind: "artifact-path" as const,
|
||||
path: module.artifact.path,
|
||||
})),
|
||||
],
|
||||
limitations: result.limitations,
|
||||
});
|
||||
);
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
resolveWakaruCommand,
|
||||
runWakaruCommand,
|
||||
} from "./WakaruCommand.js";
|
||||
import type { WakaruVersionAdmission } from "./WakaruVersion.js";
|
||||
|
||||
type PublicationContext = {
|
||||
tree: SafeOutputTree;
|
||||
@@ -34,6 +35,7 @@ type PublicationContext = {
|
||||
bytes: number;
|
||||
};
|
||||
engine: Awaited<ReturnType<typeof resolveWakaruCommand>>;
|
||||
admitted: Exclude<WakaruVersionAdmission, { status: "unsupported" }>;
|
||||
parsed: Awaited<ReturnType<typeof parseWakaruReports>>;
|
||||
execution: Awaited<ReturnType<typeof runWakaruCommand>>;
|
||||
signal?: AbortSignal;
|
||||
@@ -86,7 +88,7 @@ export const publishWakaruArtifacts = async (
|
||||
source: { ...source, published_copy: publishedCopy },
|
||||
engine: {
|
||||
id: "wakaru",
|
||||
version: WAKARU_RELEASE.version,
|
||||
version: context.admitted.version,
|
||||
executable: {
|
||||
path: context.engine.command,
|
||||
sha256: context.engine.sha256,
|
||||
@@ -107,7 +109,14 @@ export const publishWakaruArtifacts = async (
|
||||
total: parsed.report.total,
|
||||
failed: parsed.report.failed,
|
||||
modules,
|
||||
warnings: parsed.report.warnings,
|
||||
warnings: parsed.report.warnings.map(
|
||||
({ filename, kind, is_error, message }) => ({
|
||||
filename,
|
||||
kind,
|
||||
is_error,
|
||||
message,
|
||||
}),
|
||||
),
|
||||
report,
|
||||
provenance,
|
||||
analysis_input:
|
||||
@@ -118,7 +127,12 @@ export const publishWakaruArtifacts = async (
|
||||
format: "directory" as const,
|
||||
},
|
||||
runtime_equivalence: "unknown" as const,
|
||||
limitations: [...RECOVERY_LIMITATIONS],
|
||||
limitations: [
|
||||
...RECOVERY_LIMITATIONS,
|
||||
...(context.admitted.status === "compatible"
|
||||
? [context.admitted.limitation]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
const manifest = await write(
|
||||
"manifest.json",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
/** Unchanged upstream source pin; executed bytes are identified separately. */
|
||||
export const WAKARU_RELEASE = {
|
||||
version: "1.13.0",
|
||||
revision: "6070266d24b32951fa2fe1d2dad5b4313bd5c81b",
|
||||
version: "1.14.0",
|
||||
revision: "8219bf5016a063d7fb215101a8c45d5959ec733f",
|
||||
repository: "https://github.com/pionxzh/wakaru",
|
||||
} as const;
|
||||
|
||||
/** Exact provider identity shared by executions and the generated catalog. */
|
||||
/**
|
||||
* Releases the report parser accepts. Wakaru changes `--version`, `--json`
|
||||
* and `provenance.json` only by addition within a major version
|
||||
* (https://github.com/pionxzh/wakaru/blob/v1.14.0/docs/cli.md#machine-readable-output-compatibility);
|
||||
* 1.13.0 is the first release this adapter verified.
|
||||
*/
|
||||
export const WAKARU_ACCEPTED_RANGE = "^1.13.0";
|
||||
|
||||
/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */
|
||||
export const WAKARU_PROVIDER_IDENTITY = {
|
||||
id: "wakaru",
|
||||
name: "Wakaru",
|
||||
|
||||
@@ -10,20 +10,20 @@ import { readRecoveryFile, recoveryFailureMessage } from "./RecoveryFiles.js";
|
||||
import { RECOVERY_LIMITS } from "./WakaruRelease.js";
|
||||
import type { runWakaruCommand } from "./WakaruCommand.js";
|
||||
|
||||
const warningSchema = z.strictObject({
|
||||
const warningSchema = z.looseObject({
|
||||
filename: z.string(),
|
||||
kind: z.string(),
|
||||
is_error: z.boolean(),
|
||||
message: z.string(),
|
||||
});
|
||||
const reportSchema = z.strictObject({
|
||||
const reportSchema = z.looseObject({
|
||||
detected_formats: z.array(z.string()),
|
||||
safety: z.string(),
|
||||
modules: z.array(
|
||||
z.strictObject({
|
||||
z.looseObject({
|
||||
filename: z.string().min(1),
|
||||
kind: z.literal("javascript"),
|
||||
status: z.literal("decompiled"),
|
||||
status: z.string().min(1),
|
||||
}),
|
||||
),
|
||||
warnings: z.array(warningSchema),
|
||||
@@ -35,12 +35,12 @@ const rangeSchema = z.tuple([
|
||||
z.number().int().nonnegative(),
|
||||
z.number().int().nonnegative(),
|
||||
]);
|
||||
const provenanceSchema = z.strictObject({
|
||||
const provenanceSchema = z.looseObject({
|
||||
format: z.string(),
|
||||
strategy: z.string(),
|
||||
modules: z.record(
|
||||
z.string(),
|
||||
z.strictObject({
|
||||
z.looseObject({
|
||||
input: z.string(),
|
||||
ranges: z.array(rangeSchema),
|
||||
extraction: z.string(),
|
||||
@@ -59,7 +59,12 @@ type ReportContext = {
|
||||
files: readonly { relativePath: string }[];
|
||||
};
|
||||
|
||||
/** Parse exact pinned producer JSON before projecting provider-neutral artifacts. */
|
||||
/**
|
||||
* Parse producer JSON before projecting provider-neutral artifacts. Wakaru adds
|
||||
* fields and category values within a major version, so unknown keys are kept
|
||||
* and `status` stays the reported string; identity, path, range and file-set
|
||||
* checks below remain exact.
|
||||
*/
|
||||
export const parseWakaruReports = async (context: ReportContext) => {
|
||||
const { execution, provenancePath } = context;
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { admitWakaruVersion } from "./WakaruVersion.js";
|
||||
import { WAKARU_RELEASE } from "./WakaruRelease.js";
|
||||
|
||||
describe("Wakaru release range", () => {
|
||||
it("accepts the verified banner without an extra limitation", () => {
|
||||
expect(admitWakaruVersion(`wakaru ${WAKARU_RELEASE.version}`)).toEqual({
|
||||
status: "verified",
|
||||
version: WAKARU_RELEASE.version,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["wakaru 1.13.0", "1.13.0"],
|
||||
["wakaru 1.15.2", "1.15.2"],
|
||||
])("accepts %s as compatible", (banner, version) => {
|
||||
const admitted = admitWakaruVersion(banner);
|
||||
expect(admitted).toMatchObject({ status: "compatible", version });
|
||||
if (admitted.status !== "compatible")
|
||||
throw new Error("expected a compatible release");
|
||||
expect(admitted.limitation).toContain(version);
|
||||
expect(admitted.limitation).toContain(WAKARU_RELEASE.version);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["wakaru 1.12.0", "wakaru 1.12.0"],
|
||||
["wakaru 2.0.0", "wakaru 2.0.0"],
|
||||
["wakaru 1.15.0-rc.1", "wakaru 1.15.0-rc.1"],
|
||||
["wakaru v1.14.0", "wakaru v1.14.0"],
|
||||
["1.14.0", "1.14.0"],
|
||||
["", "missing"],
|
||||
])("rejects banner %j", (banner, detail) => {
|
||||
const admitted = admitWakaruVersion(banner);
|
||||
if (admitted.status !== "unsupported")
|
||||
throw new Error("expected a rejected banner");
|
||||
expect(admitted.message).toContain(detail);
|
||||
expect(admitted.message).toContain("^1.13.0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { satisfies, valid } from "semver";
|
||||
import { WAKARU_ACCEPTED_RANGE, WAKARU_RELEASE } from "./WakaruRelease.js";
|
||||
|
||||
/** Admission of one observed `wakaru --version` banner. */
|
||||
export type WakaruVersionAdmission =
|
||||
| { readonly status: "verified"; readonly version: string }
|
||||
| {
|
||||
readonly status: "compatible";
|
||||
readonly version: string;
|
||||
readonly limitation: string;
|
||||
}
|
||||
| { readonly status: "unsupported"; readonly message: string };
|
||||
|
||||
/** Caller-facing accepted releases, naming the verified build. */
|
||||
export const wakaruReleaseLine = (): string =>
|
||||
`Wakaru ${WAKARU_ACCEPTED_RANGE} (verified with ${WAKARU_RELEASE.version})`;
|
||||
|
||||
/** Classify a `wakaru <version>` banner against the accepted release range. */
|
||||
export const admitWakaruVersion = (banner: string): WakaruVersionAdmission => {
|
||||
const token = banner.startsWith("wakaru ")
|
||||
? banner.slice("wakaru ".length)
|
||||
: undefined;
|
||||
if (
|
||||
token === undefined ||
|
||||
valid(token) !== token ||
|
||||
!satisfies(token, WAKARU_ACCEPTED_RANGE)
|
||||
)
|
||||
return {
|
||||
status: "unsupported",
|
||||
message: `Unsupported Wakaru version ${banner.length === 0 ? "missing" : banner}; accepted releases are ${wakaruReleaseLine()}`,
|
||||
};
|
||||
if (token === WAKARU_RELEASE.version)
|
||||
return { status: "verified", version: token };
|
||||
return {
|
||||
status: "compatible",
|
||||
version: token,
|
||||
limitation: `This session uses Wakaru ${token}. The report parser is verified against ${WAKARU_RELEASE.version}; other ${WAKARU_ACCEPTED_RANGE} releases are accepted and the observed version is reported.`,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user