reporting-governance: harden artifact root boundary checks
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
@@ -13,6 +14,23 @@ import { createRuntimeBinding } from '../src/adapters/index.mjs';
|
||||
const packageRoot = path.resolve(import.meta.dirname, '..');
|
||||
const repoRoot = path.resolve(packageRoot, '..', '..');
|
||||
|
||||
function createArtifact(overrides = {}) {
|
||||
return {
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
runtime: 'openclaw',
|
||||
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
|
||||
scripts: { watchdog: 'scripts/long_task_watchdog.mjs' },
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('deployment profile artifact loads from package profiles and preserves compatibility envelope metadata', () => {
|
||||
const { artifactPath, artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' });
|
||||
|
||||
@@ -57,9 +75,7 @@ test('deployment profile artifact validation fails closed on boundary drift', ()
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact({
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -68,14 +84,12 @@ test('deployment profile artifact validation fails closed on boundary drift', ()
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
/spec\.bindings\.entrypoint must be a non-empty string/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact({
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '' },
|
||||
bindings: {
|
||||
@@ -84,14 +98,12 @@ test('deployment profile artifact validation fails closed on boundary drift', ()
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
/spec\.package\.pluginVersion must be a non-empty string/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact({
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -100,16 +112,14 @@ test('deployment profile artifact validation fails closed on boundary drift', ()
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
/spec\.bindings\.scripts must be an object record/
|
||||
);
|
||||
});
|
||||
|
||||
test('deployment profile artifact validation rejects absolute binding paths', () => {
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact({
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -118,14 +128,12 @@ test('deployment profile artifact validation rejects absolute binding paths', ()
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
/spec\.bindings\.entrypoint must stay within repo root: absolute paths are not allowed/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact({
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -134,16 +142,28 @@ test('deployment profile artifact validation rejects absolute binding paths', ()
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
/spec\.bindings\.scripts\.watchdog must stay within repo root: absolute paths are not allowed/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
|
||||
scripts: { watchdog: 'scripts/long_task_watchdog.mjs' },
|
||||
artifact_roots: { queueItems: '/abs/path' },
|
||||
},
|
||||
},
|
||||
})),
|
||||
/spec\.bindings\.artifact_roots\.queueItems must stay within repo root: absolute paths are not allowed/
|
||||
);
|
||||
});
|
||||
|
||||
test('deployment profile artifact validation rejects escape paths after resolution', () => {
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact({
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -152,15 +172,13 @@ test('deployment profile artifact validation rejects escape paths after resoluti
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
/spec\.bindings\.entrypoint must stay within repo root: path escapes root boundary/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => createDeploymentBindingContract({
|
||||
artifact: {
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
artifact: createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -169,18 +187,30 @@ test('deployment profile artifact validation rejects escape paths after resoluti
|
||||
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
repoRootOverride: repoRoot,
|
||||
}),
|
||||
/spec\.bindings\.scripts\.watchdog must stay within repo root: path escapes root boundary/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
|
||||
scripts: { watchdog: 'scripts/long_task_watchdog.mjs' },
|
||||
artifact_roots: { queueItems: '../escape' },
|
||||
},
|
||||
},
|
||||
})),
|
||||
/spec\.bindings\.artifact_roots\.queueItems must stay within repo root: path escapes root boundary/
|
||||
);
|
||||
});
|
||||
|
||||
test('deployment binding contract allows normalized in-root paths that contain dot segments', () => {
|
||||
const binding = createDeploymentBindingContract({
|
||||
artifact: {
|
||||
kind: 'DeploymentProfileArtifact',
|
||||
apiVersion: 'reporting-governance/v1alpha1',
|
||||
artifact: createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
@@ -194,7 +224,7 @@ test('deployment binding contract allows normalized in-root paths that contain d
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
repoRootOverride: repoRoot,
|
||||
});
|
||||
|
||||
@@ -202,3 +232,30 @@ test('deployment binding contract allows normalized in-root paths that contain d
|
||||
assert.equal(binding.scripts.watchdog, path.resolve(repoRoot, 'scripts/long_task_watchdog.mjs'));
|
||||
assert.equal(binding.artifactRoots.queueItems, path.resolve(repoRoot, 'state/operator-notify-queue'));
|
||||
});
|
||||
|
||||
test('deployment profile artifact validation rejects artifact_roots symlink escape after realpath resolution', async (t) => {
|
||||
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-profile-artifact-'));
|
||||
t.after(() => fs.rmSync(sandbox, { recursive: true, force: true }));
|
||||
|
||||
const fakeRepoRoot = path.join(sandbox, 'repo');
|
||||
const outsideRoot = path.join(sandbox, 'outside');
|
||||
fs.mkdirSync(fakeRepoRoot, { recursive: true });
|
||||
fs.mkdirSync(outsideRoot, { recursive: true });
|
||||
fs.symlinkSync(outsideRoot, path.join(fakeRepoRoot, 'state-link'), 'dir');
|
||||
fs.writeFileSync(path.join(fakeRepoRoot, 'entry.mjs'), 'export default true;\n');
|
||||
fs.writeFileSync(path.join(fakeRepoRoot, 'watchdog.mjs'), 'export default true;\n');
|
||||
|
||||
assert.throws(
|
||||
() => validateDeploymentProfileArtifact(createArtifact({
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
bindings: {
|
||||
entrypoint: 'entry.mjs',
|
||||
scripts: { watchdog: 'watchdog.mjs' },
|
||||
artifact_roots: { queueItems: 'state-link/queue' },
|
||||
},
|
||||
},
|
||||
}), { repoRootOverride: fakeRepoRoot }),
|
||||
/spec\.bindings\.artifact_roots\.queueItems must stay within repo root: symlink resolution escapes realpath boundary/
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user