Source commit: 6d1a121b0a8a636aac95998fe5d15c24c78eb7d0 Public tree identity: sha256:2bc22807f5b838286678b65d3f550b8ae4714ae673622041d4c2516a7c5b7926
184 lines
7.3 KiB
JavaScript
184 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const repo = path.resolve(__dirname, "..");
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
|
}
|
|
|
|
function maybeRead(segments) {
|
|
const fullPath = path.join(repo, ...segments);
|
|
if (!fs.existsSync(fullPath)) return null;
|
|
return fs.readFileSync(fullPath, "utf8");
|
|
}
|
|
|
|
function expect(source, name, pattern) {
|
|
assert.match(source, pattern, `missing hostile-input evidence: ${name}`);
|
|
}
|
|
|
|
const coreSource = read("crates/clusterflux-core/src/source.rs");
|
|
const coreCapabilities = read("crates/clusterflux-core/src/capability.rs");
|
|
const coordinatorService = [
|
|
read("crates/clusterflux-coordinator/src/service.rs"),
|
|
read("crates/clusterflux-coordinator/src/service/routing.rs"),
|
|
read("crates/clusterflux-coordinator/src/service/signed_nodes.rs"),
|
|
read("crates/clusterflux-coordinator/src/service/logs.rs"),
|
|
read("crates/clusterflux-coordinator/src/service/tests.rs"),
|
|
].join("\n");
|
|
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
|
|
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
|
|
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
|
|
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
|
|
|
|
for (const [name, pattern] of [
|
|
["source manifests validate shape", /pub fn validate_public_mvp\(&self\)[\s\S]*self\.validate_shape\(\)\?/],
|
|
["source manifests reject invalid digests", /SourceManifestError::InvalidDigest/],
|
|
["source manifests reject invalid custom providers", /SourceManifestError::InvalidProviderId/],
|
|
["source manifests reject control characters", /DescriptionControlCharacter/],
|
|
["source manifests reject coordinator checkout access", /CoordinatorCheckoutAccess/],
|
|
["source manifests reject default source-byte upload", /CoordinatorReceivesSourceBytes/],
|
|
]) {
|
|
expect(coreSource, name, pattern);
|
|
}
|
|
|
|
for (const [name, pattern] of [
|
|
["capability reports validate public shape", /pub fn validate_public_report\(&self\)/],
|
|
["capability reports validate architecture labels", /InvalidArchitecture/],
|
|
["capability reports validate OS labels", /InvalidOsLabel/],
|
|
["capability reports validate source providers", /InvalidSourceProvider/],
|
|
["source provider ids reject path traversal", /valid_source_provider_id/],
|
|
]) {
|
|
expect(coreCapabilities, name, pattern);
|
|
}
|
|
|
|
for (const [name, pattern] of [
|
|
["coordinator task log tails are bounded", /MAX_TASK_LOG_TAIL_BYTES: usize = 256 \* 1024/],
|
|
["coordinator validates reported stdout tails", /ReportTaskLog[\s\S]*validate_task_log_tail\("stdout_tail", &stdout_tail\)\?/],
|
|
["coordinator validates completed task stdout tails", /TaskCompleted[\s\S]*validate_task_log_tail\("stdout_tail", &stdout_tail\)\?/],
|
|
["coordinator rejects oversized log tail in unit coverage", /"x"\.repeat\(MAX_TASK_LOG_TAIL_BYTES \+ 1\)/],
|
|
]) {
|
|
expect(coordinatorService, name, pattern);
|
|
}
|
|
|
|
for (const [name, pattern] of [
|
|
["service rejects malformed node capability report", /fn service_rejects_malformed_node_capability_report\(\)/],
|
|
["capability report rejection leaves descriptors empty", /assert!\(service\.node_descriptors\.is_empty\(\)\)/],
|
|
["node capability report rejects cross-scope writes", /fn service_rejects_node_capability_report_outside_enrollment_scope\(\)/],
|
|
["task completion rejects cross-scope writes", /task completion outside node scope|outside/],
|
|
]) {
|
|
expect(coordinatorService, name, pattern);
|
|
}
|
|
|
|
for (const [name, source, patterns] of [
|
|
[
|
|
"artifact download smoke",
|
|
artifactDownloadSmoke,
|
|
[
|
|
/const crossTenant = await send/,
|
|
/const crossProject = await send/,
|
|
/const guessed = await send/,
|
|
/const crossActorOpen = await send/,
|
|
/token is invalid/,
|
|
/tenant mismatch/,
|
|
/project mismatch/,
|
|
],
|
|
],
|
|
[
|
|
"operator panel smoke",
|
|
operatorPanelSmoke,
|
|
[
|
|
/render_operator_panel/,
|
|
/submit_panel_event/,
|
|
/assert\(!JSON\.stringify\(panel\)\.includes\("<script"\)\)/,
|
|
/assert\(!JSON\.stringify\(panel\)\.toLowerCase\(\)\.includes\("oauth"\)\)/,
|
|
/rate limit/i,
|
|
/exceeds download limit/,
|
|
],
|
|
],
|
|
[
|
|
"scheduler smoke",
|
|
schedulerSmoke,
|
|
[/const crossTenantReport = await send/, /report_node_capabilities/, /tenant\\\/project scope/],
|
|
],
|
|
[
|
|
"source preparation smoke",
|
|
sourcePreparationSmoke,
|
|
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
|
|
],
|
|
]) {
|
|
for (const pattern of patterns) {
|
|
expect(source, name, pattern);
|
|
}
|
|
}
|
|
|
|
const hostedServiceMain = maybeRead([
|
|
"private",
|
|
"hosted-policy",
|
|
"src",
|
|
"bin",
|
|
"clusterflux-hosted-service.rs",
|
|
]);
|
|
const hostedValidation = maybeRead([
|
|
"private",
|
|
"hosted-policy",
|
|
"src",
|
|
"bin",
|
|
"clusterflux-hosted-service",
|
|
"validation.rs",
|
|
]);
|
|
const hostedWire = maybeRead([
|
|
"private",
|
|
"hosted-policy",
|
|
"src",
|
|
"bin",
|
|
"clusterflux-hosted-service",
|
|
"wire.rs",
|
|
]);
|
|
const hostedProtocol = maybeRead([
|
|
"private",
|
|
"hosted-policy",
|
|
"src",
|
|
"bin",
|
|
"clusterflux-hosted-service",
|
|
"hosted_service_protocol.rs",
|
|
]);
|
|
const hostedService =
|
|
hostedServiceMain && hostedValidation && hostedWire && hostedProtocol
|
|
? [hostedServiceMain, hostedValidation, hostedWire, hostedProtocol].join("\n")
|
|
: null;
|
|
const hostedTests = [
|
|
maybeRead(["private", "hosted-policy", "src", "bin", "clusterflux-hosted-service", "tests.rs"]),
|
|
maybeRead(["private", "hosted-policy", "scripts", "hosted-deployment-smoke.js"]),
|
|
maybeRead(["private", "hosted-policy", "scripts", "hosted-client-compat-smoke.js"]),
|
|
].filter(Boolean).join("\n");
|
|
|
|
if (hostedService && hostedTests) {
|
|
for (const [name, pattern] of [
|
|
["hosted service turns malformed JSON into error responses", /decode_incoming_request[\s\S]*HostedServiceResponse::Error/],
|
|
["tenant ids are validated", /fn tenant_id\(value: String\)[\s\S]*validate_identifier\("tenant", &value\)\?/],
|
|
["node ids are validated", /fn node_id\(value: String\)[\s\S]*validate_identifier\("node", &value\)\?/],
|
|
["process ids are validated", /fn process_id\(value: String\)[\s\S]*validate_identifier\("process", &value\)\?/],
|
|
["identifiers reject empty and control/path characters", /fn validate_identifier[\s\S]*trim\(\)\.is_empty\(\)[\s\S]*ch\.is_control\(\) \|\| ch == '\/' \|\| ch == '\\\\'/],
|
|
["OIDC text fields are bounded", /fn validate_text[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
|
|
["tokens are bounded", /fn validate_token[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
|
|
["control request bodies are bounded", /MAX_CONTROL_FRAME_BYTES[\s\S]*control request too large/],
|
|
["identity protocol rejects unknown authority fields", /deny_unknown_fields/],
|
|
]) {
|
|
expect(hostedService, name, pattern);
|
|
}
|
|
|
|
for (const [name, pattern] of [
|
|
["old client identity protocol is rejected", /hosted_login_protocol_rejects_client_identity_and_provider_configuration/],
|
|
["raw operator action is rejected", /rawOperatorDenied[\s\S]*hosted_operator_request envelope/],
|
|
["unsigned client identity is rejected", /const forged = await sendHostedControl[\s\S]*authenticated CLI session/],
|
|
["cross-tenant process inspection is rejected", /crossTenantTaskEventsDenied[\s\S]*scope\|denied\|unauthorized/],
|
|
]) {
|
|
expect(hostedTests, name, pattern);
|
|
}
|
|
}
|
|
|
|
console.log("Hostile input contract smoke passed");
|