mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(apk): classify fixed inspection allowances as nonretryable (#7784)
This commit is contained in:
1 parent
9593bc536b
commit
ac545adae2
5 files changed
+191
-3
No files matched your search
@@ -239,6 +239,13 @@ serialized size of the report together with its limitations (which repeat
|
||||
archive names) is then checked against an 8 MiB output budget (the exact check
|
||||
is authoritative); an over-budget report is refused, not truncated.
|
||||
|
||||
Signing blocks admit at most 4096 ID/value pairs; at most 4096 signing-relevant
|
||||
META-INF entries are retained. These fixed counts, signer/lineage counts and the
|
||||
report byte budget return nonretryable `resource_constraint` errors naming the
|
||||
effective allowance. This release has no runtime override for them. Waiting for
|
||||
an active scan cannot raise a fixed count, and complete CLI JSON cannot raise the
|
||||
report budget. Occupied scan-queue capacity remains retryable after a scan finishes.
|
||||
|
||||
The whole-file reader allocates the selected snapshot and bounded read scratch;
|
||||
total process memory exceeds the selected file's size. It also computes the complete
|
||||
file SHA-256, so inspection is not header-only. Content digests are recomputed
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { apkSignatureAdmission } from "./ApkSignatureAdmission.js";
|
||||
import { APK_SIGNATURE_LIMITS } from "./ApkSignatureRelease.js";
|
||||
@@ -21,9 +23,19 @@ it("bounds process-wide scans and waiting requests, cancels queued work and reco
|
||||
() => apkSignatureAdmission.acquire(),
|
||||
);
|
||||
try {
|
||||
await expect(apkSignatureAdmission.acquire()).rejects.toMatchObject({
|
||||
_tag: "AnalysisResourceConstraintError",
|
||||
resource: "capacity",
|
||||
const refused = await apkSignatureAdmission
|
||||
.acquire()
|
||||
.catch((cause: unknown) => {
|
||||
if (!(cause instanceof AnalysisResourceConstraintError)) throw cause;
|
||||
return projectAnalysisError(cause);
|
||||
});
|
||||
expect(refused).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
retryable: true,
|
||||
remediation: {
|
||||
action:
|
||||
"Wait for an active analysis to complete, then retry within the reported admission limits.",
|
||||
},
|
||||
});
|
||||
expect(granted).toBe(false);
|
||||
controller.abort(reason);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
apkOutputBudgetError,
|
||||
mapApkBlockError,
|
||||
mapApkSchemeError,
|
||||
mapCentralDirectoryError,
|
||||
v1SignerCapacityError,
|
||||
} from "./apkSignatureFailures.js";
|
||||
import { APK_SIGNATURE_LIMITS } from "./ApkSignatureRelease.js";
|
||||
|
||||
it.each([
|
||||
[
|
||||
"block pairs",
|
||||
mapApkBlockError(
|
||||
{ kind: "budget-exceeded", resource: "signing-block-pairs", limit: 4096 },
|
||||
"/selected.apk",
|
||||
),
|
||||
"max_signing_block_pairs",
|
||||
4096,
|
||||
],
|
||||
[
|
||||
"scheme signers",
|
||||
mapApkSchemeError(
|
||||
{ kind: "budget-exceeded", resource: "signers", limit: 32 },
|
||||
"v2",
|
||||
),
|
||||
"max_signers",
|
||||
32,
|
||||
],
|
||||
[
|
||||
"v1 entries",
|
||||
mapCentralDirectoryError({
|
||||
kind: "budget-exceeded",
|
||||
resource: "v1-signature-entries",
|
||||
limit: 4096,
|
||||
}),
|
||||
"max_v1_signature_entries",
|
||||
4096,
|
||||
],
|
||||
["v1 signers", v1SignerCapacityError(32), "max_v1_signers", 32],
|
||||
[
|
||||
"report bytes",
|
||||
apkOutputBudgetError(APK_SIGNATURE_LIMITS.outputBytes + 1),
|
||||
"max_output_bytes",
|
||||
APK_SIGNATURE_LIMITS.outputBytes,
|
||||
],
|
||||
] as const)(
|
||||
"reports %s as a deterministic fixed allowance",
|
||||
(_name, error, key, limit) => {
|
||||
const projected = projectAnalysisError(error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
retryable: false,
|
||||
details: { reported_limits: { [key]: limit } },
|
||||
});
|
||||
expect(projected.remediation.action).toContain(String(limit));
|
||||
expect(projected.remediation.action).toContain("no runtime override");
|
||||
expect(projected.remediation.action).toContain(
|
||||
"Repeating the unchanged request fails the same way",
|
||||
);
|
||||
expect(projected.remediation.action).not.toContain(
|
||||
"Wait for an active analysis to complete",
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
@@ -66,6 +67,10 @@ export const mapApkBlockError = (
|
||||
"capacity",
|
||||
`APK Signing Block holds more ${error.resource} than the ${String(error.limit)} limit.`,
|
||||
{ max_signing_block_pairs: error.limit },
|
||||
fixedAllowanceOptions(
|
||||
`The APK Signing Block allowance of ${String(error.limit)} pairs (this release has no runtime override)`,
|
||||
"count",
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -84,6 +89,10 @@ export const mapApkSchemeError = (
|
||||
"capacity",
|
||||
`APK Signature Scheme ${scheme} holds more ${error.resource} than the ${String(error.limit)} limit.`,
|
||||
{ [`max_${error.resource.replace(/-/g, "_")}`]: error.limit },
|
||||
fixedAllowanceOptions(
|
||||
`The APK Signature Scheme ${scheme} allowance of ${String(error.limit)} ${error.resource} (this release has no runtime override)`,
|
||||
"count",
|
||||
),
|
||||
);
|
||||
|
||||
/** A central directory that cannot be walked leaves v1 presence unknown: refuse. */
|
||||
@@ -96,6 +105,10 @@ export const mapCentralDirectoryError = (
|
||||
"capacity",
|
||||
`The central directory holds more than ${String(error.limit)} META-INF v1 signing entries.`,
|
||||
{ max_v1_signature_entries: error.limit },
|
||||
fixedAllowanceOptions(
|
||||
`The APK v1 allowance of ${String(error.limit)} META-INF signing entries (this release has no runtime override)`,
|
||||
"count",
|
||||
),
|
||||
)
|
||||
: inputError(
|
||||
`Malformed ZIP central directory at byte ${String(error.offset)}: ${error.detail}.`,
|
||||
@@ -108,6 +121,10 @@ export const v1SignerCapacityError = (limit: number): AnalysisError =>
|
||||
"capacity",
|
||||
`The archive pairs more than ${String(limit)} v1 signature blocks with .SF files.`,
|
||||
{ max_v1_signers: limit },
|
||||
fixedAllowanceOptions(
|
||||
`The APK v1 allowance of ${String(limit)} signers (this release has no runtime override)`,
|
||||
"count",
|
||||
),
|
||||
);
|
||||
|
||||
/** The decoded result would exceed the inline output budget. */
|
||||
@@ -117,6 +134,10 @@ export const apkOutputBudgetError = (projectedBytes: number): AnalysisError =>
|
||||
"capacity",
|
||||
`The decoded signing report (${String(projectedBytes)} bytes) exceeds the ${String(APK_SIGNATURE_LIMITS.outputBytes)}-byte output budget.`,
|
||||
{ max_output_bytes: APK_SIGNATURE_LIMITS.outputBytes },
|
||||
fixedAllowanceOptions(
|
||||
`The decoded APK signing report allowance of ${String(APK_SIGNATURE_LIMITS.outputBytes)} bytes (this release has no runtime override)`,
|
||||
"report",
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { parseEvidence } from "../../src/domain/evidence.js";
|
||||
import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js";
|
||||
import { apkSignatureSummaryViewResultSchema } from "../../src/domain/analysisView/analysisView.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
withSigningBlock,
|
||||
zipWith,
|
||||
} from "../fixtures/apkSignature/v1SignatureBlock.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"keeps fixed APK pair refusals nonretryable through CLI and MCP, then accepts a valid input",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-apk-pair-limit-");
|
||||
const archive = await zipWith([
|
||||
{ name: "AndroidManifest.xml", data: Uint8Array.of(3, 0, 8, 0) },
|
||||
]);
|
||||
const paths = new Map<number, string>();
|
||||
const digests = new Map<number, string>();
|
||||
for (const count of [1, 4096, 4097]) {
|
||||
const path = join(root, `pairs-${count}.apk`);
|
||||
const bytes = withSigningBlock(
|
||||
archive,
|
||||
Array.from({ length: count }, () => ({
|
||||
id: 0xffffffff,
|
||||
value: new Uint8Array(),
|
||||
})),
|
||||
);
|
||||
await writeFile(path, bytes);
|
||||
digests.set(count, createHash("sha256").update(bytes).digest("hex"));
|
||||
paths.set(count, path);
|
||||
}
|
||||
const expected = {
|
||||
code: "resource_constraint",
|
||||
category: "resource_constraint",
|
||||
retryable: false,
|
||||
details: {
|
||||
operation: "inspect_apk_signature",
|
||||
resource: "capacity",
|
||||
reported_limits: { max_signing_block_pairs: 4096 },
|
||||
},
|
||||
};
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const reply = await cli.run({
|
||||
arguments: ["inspect-apk-signature", paths.get(4097)!, "--json"],
|
||||
});
|
||||
expect(reply.exitCode, reply.stderr).toBe(1);
|
||||
const error = analysisErrorProjectionSchema.parse(reply.json);
|
||||
expect(error).toMatchObject(expected);
|
||||
expect(error.remediation.action).toContain("4096");
|
||||
expect(error.remediation.action).toContain("no runtime override");
|
||||
expect(error.remediation.action).toContain(
|
||||
"Waiting for an active analysis to finish does not raise it",
|
||||
);
|
||||
}
|
||||
const { call } = await connectLocalToolsMcp({
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
|
||||
});
|
||||
const rejected = await call("inspect_apk_signature", {
|
||||
path: paths.get(4097),
|
||||
});
|
||||
expect(parseMcpToolError(rejected).error).toMatchObject(expected);
|
||||
for (const count of [4096, 1]) {
|
||||
const path = paths.get(count)!;
|
||||
const reply = await call("inspect_apk_signature", { path });
|
||||
expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true);
|
||||
const report = apkSignatureSummaryViewResultSchema.parse(
|
||||
parseEvidence(reply.structuredContent).normalized_result,
|
||||
).summary;
|
||||
expect(report.artifact.path).toBe(path);
|
||||
expect(report.artifact.sha256).toBe(digests.get(count));
|
||||
expect(report.posture.strongest_scheme).toBe("none");
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user