feat: Implement Filesystem and MongoDB provenance writers for PackRun execution context
- Added `FilesystemPackRunProvenanceWriter` to write provenance manifests to the filesystem. - Introduced `MongoPackRunArtifactReader` to read artifacts from MongoDB. - Created `MongoPackRunProvenanceWriter` to store provenance manifests in MongoDB. - Developed unit tests for filesystem and MongoDB provenance writers. - Established `ITimelineEventStore` and `ITimelineIngestionService` interfaces for timeline event handling. - Implemented `TimelineIngestionService` to validate and persist timeline events with hashing. - Created PostgreSQL schema and migration scripts for timeline indexing. - Added dependency injection support for timeline indexer services. - Developed tests for timeline ingestion and schema validation.
This commit is contained in:
38
bench/reachability-benchmark/cases/js/express-eval/case.yaml
Normal file
38
bench/reachability-benchmark/cases/js/express-eval/case.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
id: "js-express-eval:003"
|
||||
language: js
|
||||
project: express-eval
|
||||
version: "1.0.0"
|
||||
description: "Admin exec endpoint evaluates user code"
|
||||
entrypoints:
|
||||
- "POST /api/admin/exec"
|
||||
sinks:
|
||||
- id: "ExpressEval::exec"
|
||||
path: "src/app.js::createServer"
|
||||
kind: "process"
|
||||
location:
|
||||
file: src/app.js
|
||||
line: 17
|
||||
notes: "eval(code) on admin path"
|
||||
environment:
|
||||
os_image: "node:20-alpine"
|
||||
runtime:
|
||||
node: "20.11.0"
|
||||
source_date_epoch: 1730000000
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Admin exec endpoint reachable and executes eval"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/js-express-eval.json"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "js-express-eval:003"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/admin/exec"
|
||||
route: "/api/admin/exec"
|
||||
method: "POST"
|
||||
handler: "createServer.exec"
|
||||
description: "Admin-only exec (reachable)"
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "rb-case-express-eval",
|
||||
"version": "1.0.0",
|
||||
"description": "Reachability benchmark case: express-like admin eval endpoint",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "./tests/run-tests.sh"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
// Minimal express-like router.
|
||||
function makeApp() {
|
||||
const routes = {};
|
||||
return {
|
||||
post(path, handler) {
|
||||
routes[`POST ${path}`] = handler;
|
||||
},
|
||||
handle(method, path, req, res) {
|
||||
const key = `${method} ${path}`;
|
||||
if (routes[key]) {
|
||||
return routes[key](req, res);
|
||||
}
|
||||
return { status: 404, body: 'not found' };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createServer() {
|
||||
const app = makeApp();
|
||||
app.post('/api/admin/exec', (req) => {
|
||||
if (!req || typeof req.body?.code !== 'string') {
|
||||
return { status: 400, body: 'bad request' };
|
||||
}
|
||||
// Sink: eval on admin endpoint (reachable)
|
||||
// eslint-disable-next-line no-eval
|
||||
const result = eval(req.body.code);
|
||||
return { status: 200, body: String(result) };
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = { createServer };
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1730000000}
|
||||
export TZ=UTC
|
||||
export LC_ALL=C
|
||||
node test_reach.js
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createServer } = require('../src/app');
|
||||
|
||||
const OUT_DIR = path.resolve(__dirname, '../outputs');
|
||||
const TRACE_DIR = path.join(OUT_DIR, 'traces');
|
||||
const COVERAGE_FILE = path.join(OUT_DIR, 'coverage.json');
|
||||
const TRACE_FILE = path.join(TRACE_DIR, 'traces.json');
|
||||
|
||||
function ensureDirs() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.mkdirSync(TRACE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function recordTrace(entry, pathNodes) {
|
||||
fs.writeFileSync(
|
||||
TRACE_FILE,
|
||||
JSON.stringify({
|
||||
entry,
|
||||
path: pathNodes,
|
||||
sink: 'ExpressEval::exec',
|
||||
notes: 'Admin exec reached'
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function recordCoverage(filePath, lines) {
|
||||
fs.writeFileSync(
|
||||
COVERAGE_FILE,
|
||||
JSON.stringify({
|
||||
files: {
|
||||
[filePath]: {
|
||||
lines_covered: lines,
|
||||
lines_total: 40
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
(function main() {
|
||||
ensureDirs();
|
||||
const app = createServer();
|
||||
const res = app.handle('POST', '/api/admin/exec', { body: { code: '21*2' } });
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body, '42');
|
||||
|
||||
recordTrace('POST /api/admin/exec', ['app.js::createServer', 'handler', 'eval(code)']);
|
||||
recordCoverage('src/app.js', [5, 6, 7, 13, 18, 19]);
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'SINK_REACHED'), 'true');
|
||||
})();
|
||||
@@ -0,0 +1,38 @@
|
||||
id: "js-express-guarded:004"
|
||||
language: js
|
||||
project: express-guarded
|
||||
version: "1.0.0"
|
||||
description: "Admin exec guarded by ALLOW_EXEC flag; unreachable by default"
|
||||
entrypoints:
|
||||
- "POST /api/admin/exec"
|
||||
sinks:
|
||||
- id: "ExpressGuarded::exec"
|
||||
path: "src/app.js::createServer"
|
||||
kind: "process"
|
||||
location:
|
||||
file: src/app.js
|
||||
line: 16
|
||||
notes: "eval(code) gated by ALLOW_EXEC"
|
||||
environment:
|
||||
os_image: "node:20-alpine"
|
||||
runtime:
|
||||
node: "20.11.0"
|
||||
source_date_epoch: 1730000000
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Guard prevents sink unless ALLOW_EXEC=true"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/js-express-guarded.json"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "js-express-guarded:004"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/admin/exec"
|
||||
route: "/api/admin/exec"
|
||||
method: "POST"
|
||||
handler: "createServer.exec"
|
||||
description: "Admin exec blocked unless ALLOW_EXEC=true"
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "rb-case-express-guarded",
|
||||
"version": "1.0.0",
|
||||
"description": "Reachability benchmark case: express-like admin exec guarded by env flag",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "./tests/run-tests.sh"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
function makeApp() {
|
||||
const routes = {};
|
||||
return {
|
||||
post(path, handler) {
|
||||
routes[`POST ${path}`] = handler;
|
||||
},
|
||||
handle(method, path, req) {
|
||||
const key = `${method} ${path}`;
|
||||
if (routes[key]) return routes[key](req);
|
||||
return { status: 404, body: 'not found' };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createServer() {
|
||||
const app = makeApp();
|
||||
app.post('/api/admin/exec', (req) => {
|
||||
if (req?.env?.ALLOW_EXEC !== 'true') {
|
||||
return { status: 403, body: 'forbidden' };
|
||||
}
|
||||
if (typeof req?.body?.code !== 'string') {
|
||||
return { status: 400, body: 'bad request' };
|
||||
}
|
||||
// eslint-disable-next-line no-eval
|
||||
const result = eval(req.body.code);
|
||||
return { status: 200, body: String(result) };
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = { createServer };
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1730000000}
|
||||
export TZ=UTC
|
||||
export LC_ALL=C
|
||||
node test_unreachable.js
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createServer } = require('../src/app');
|
||||
|
||||
const OUT_DIR = path.resolve(__dirname, '../outputs');
|
||||
const TRACE_DIR = path.join(OUT_DIR, 'traces');
|
||||
const COVERAGE_FILE = path.join(OUT_DIR, 'coverage.json');
|
||||
const TRACE_FILE = path.join(TRACE_DIR, 'traces.json');
|
||||
|
||||
function ensureDirs() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.mkdirSync(TRACE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function recordTrace(entry, pathNodes) {
|
||||
fs.writeFileSync(
|
||||
TRACE_FILE,
|
||||
JSON.stringify({
|
||||
entry,
|
||||
path: pathNodes,
|
||||
sink: 'ExpressGuarded::exec',
|
||||
notes: 'Guard blocked sink'
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function recordCoverage(filePath, lines) {
|
||||
fs.writeFileSync(
|
||||
COVERAGE_FILE,
|
||||
JSON.stringify({
|
||||
files: {
|
||||
[filePath]: {
|
||||
lines_covered: lines,
|
||||
lines_total: 50
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
(function main() {
|
||||
ensureDirs();
|
||||
const app = createServer();
|
||||
const res = app.handle('POST', '/api/admin/exec', { body: { code: '2+2' }, env: { ALLOW_EXEC: 'false' } });
|
||||
assert.strictEqual(res.status, 403);
|
||||
assert.strictEqual(res.body, 'forbidden');
|
||||
|
||||
recordTrace('POST /api/admin/exec', ['app.js::createServer', 'guard: ALLOW_EXEC!=true']);
|
||||
recordCoverage('src/app.js', [5,6,7,12,13,14,15]);
|
||||
})();
|
||||
@@ -0,0 +1,38 @@
|
||||
id: "js-fastify-template:005"
|
||||
language: js
|
||||
project: fastify-template
|
||||
version: "1.0.0"
|
||||
description: "Template rendering route replaces user placeholder"
|
||||
entrypoints:
|
||||
- "POST /api/render"
|
||||
sinks:
|
||||
- id: "FastifyTemplate::render"
|
||||
path: "src/app.js::createServer"
|
||||
kind: "http"
|
||||
location:
|
||||
file: src/app.js
|
||||
line: 19
|
||||
notes: "Template rendering of user input"
|
||||
environment:
|
||||
os_image: "node:20-alpine"
|
||||
runtime:
|
||||
node: "20.11.0"
|
||||
source_date_epoch: 1730000000
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Template rendering reachable via POST /api/render"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/js-fastify-template.json"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "js-fastify-template:005"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/render"
|
||||
route: "/api/render"
|
||||
method: "POST"
|
||||
handler: "createServer.render"
|
||||
description: "Template rendering endpoint"
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "rb-case-fastify-template",
|
||||
"version": "1.0.0",
|
||||
"description": "Reachability benchmark case: fastify-like template rendering",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "./tests/run-tests.sh"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// Simulated Fastify route registration for template injection.
|
||||
function buildServer() {
|
||||
const routes = {};
|
||||
return {
|
||||
post(path, handler) {
|
||||
routes[`POST ${path}`] = handler;
|
||||
},
|
||||
inject(method, path, payload) {
|
||||
const key = `${method} ${path}`;
|
||||
const handler = routes[key];
|
||||
if (!handler) return { status: 404, body: 'not found' };
|
||||
return handler({ body: payload });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createServer() {
|
||||
const server = buildServer();
|
||||
server.post('/api/render', (req) => {
|
||||
const template = req?.body?.template;
|
||||
if (typeof template !== 'string') {
|
||||
return { status: 400, body: 'bad request' };
|
||||
}
|
||||
const compiled = template.replace('{{user}}', 'guest');
|
||||
// Sink: writes rendered content to log (simulated SSR)
|
||||
return { status: 200, body: compiled };
|
||||
});
|
||||
return server;
|
||||
}
|
||||
|
||||
module.exports = { createServer };
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1730000000}
|
||||
export TZ=UTC
|
||||
export LC_ALL=C
|
||||
node test_reach.js
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createServer } = require('../src/app');
|
||||
|
||||
const OUT_DIR = path.resolve(__dirname, '../outputs');
|
||||
const TRACE_DIR = path.join(OUT_DIR, 'traces');
|
||||
const COVERAGE_FILE = path.join(OUT_DIR, 'coverage.json');
|
||||
const TRACE_FILE = path.join(TRACE_DIR, 'traces.json');
|
||||
|
||||
function ensureDirs() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.mkdirSync(TRACE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function recordTrace(entry, pathNodes) {
|
||||
fs.writeFileSync(
|
||||
TRACE_FILE,
|
||||
JSON.stringify({
|
||||
entry,
|
||||
path: pathNodes,
|
||||
sink: 'FastifyTemplate::render',
|
||||
notes: 'Template rendered with user input'
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function recordCoverage(filePath, lines) {
|
||||
fs.writeFileSync(
|
||||
COVERAGE_FILE,
|
||||
JSON.stringify({
|
||||
files: {
|
||||
[filePath]: {
|
||||
lines_covered: lines,
|
||||
lines_total: 45
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
(function main() {
|
||||
ensureDirs();
|
||||
const server = createServer();
|
||||
const res = server.inject('POST', '/api/render', { template: 'Hello {{user}}' });
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body, 'Hello guest');
|
||||
|
||||
recordTrace('POST /api/render', ['app.js::createServer', 'render template']);
|
||||
recordCoverage('src/app.js', [5,6,7,13,18,20]);
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'SINK_REACHED'), 'true');
|
||||
})();
|
||||
38
bench/reachability-benchmark/cases/js/guarded-eval/case.yaml
Normal file
38
bench/reachability-benchmark/cases/js/guarded-eval/case.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
id: "js-guarded-eval:002"
|
||||
language: js
|
||||
project: guarded-eval
|
||||
version: "1.0.0"
|
||||
description: "Eval sink guarded by FEATURE_ENABLE flag; unreachable when flag is off"
|
||||
entrypoints:
|
||||
- "POST /api/exec"
|
||||
sinks:
|
||||
- id: "GuardedEval::handleRequest"
|
||||
path: "src/app.js::handleRequest"
|
||||
kind: "process"
|
||||
location:
|
||||
file: src/app.js
|
||||
line: 13
|
||||
notes: "eval on user input guarded by FEATURE_ENABLE"
|
||||
environment:
|
||||
os_image: "node:20-alpine"
|
||||
runtime:
|
||||
node: "20.11.0"
|
||||
source_date_epoch: 1730000000
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Guard prevents sink when FEATURE_ENABLE != 1"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/js-guarded-eval.json"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "js-guarded-eval:002"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/exec"
|
||||
route: "/api/exec"
|
||||
method: "POST"
|
||||
handler: "app.js::handleRequest"
|
||||
description: "Feature-flagged code execution endpoint"
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "rb-case-guarded-eval",
|
||||
"version": "1.0.0",
|
||||
"description": "Reachability benchmark case: eval guarded by feature flag",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "./tests/run-tests.sh"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
function handleRequest(body, env = process.env) {
|
||||
if (env.FEATURE_ENABLE !== '1') {
|
||||
return { status: 403, body: 'disabled' };
|
||||
}
|
||||
|
||||
const code = body && body.code;
|
||||
if (typeof code !== 'string') {
|
||||
return { status: 400, body: 'bad request' };
|
||||
}
|
||||
|
||||
// This sink is reachable only when FEATURE_ENABLE=1.
|
||||
// eslint-disable-next-line no-eval
|
||||
const result = eval(code);
|
||||
return { status: 200, body: String(result) };
|
||||
}
|
||||
|
||||
module.exports = { handleRequest };
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1730000000}
|
||||
export TZ=UTC
|
||||
export LC_ALL=C
|
||||
node test_unreachable.js
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { handleRequest } = require('../src/app');
|
||||
|
||||
const OUT_DIR = path.resolve(__dirname, '../outputs');
|
||||
const TRACE_DIR = path.join(OUT_DIR, 'traces');
|
||||
const COVERAGE_FILE = path.join(OUT_DIR, 'coverage.json');
|
||||
const TRACE_FILE = path.join(TRACE_DIR, 'traces.json');
|
||||
|
||||
function ensureDirs() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.mkdirSync(TRACE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function recordTrace(entry, pathNodes) {
|
||||
fs.writeFileSync(
|
||||
TRACE_FILE,
|
||||
JSON.stringify({
|
||||
entry,
|
||||
path: pathNodes,
|
||||
sink: 'GuardedEval::handleRequest',
|
||||
notes: 'Guard prevented sink execution'
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function recordCoverage(filePath, lines) {
|
||||
fs.writeFileSync(
|
||||
COVERAGE_FILE,
|
||||
JSON.stringify({
|
||||
files: {
|
||||
[filePath]: {
|
||||
lines_covered: lines,
|
||||
lines_total: 32
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
(function main() {
|
||||
ensureDirs();
|
||||
const payload = { code: '1 + 2' };
|
||||
const response = handleRequest(payload, { FEATURE_ENABLE: '0' });
|
||||
assert.strictEqual(response.status, 403);
|
||||
assert.strictEqual(response.body, 'disabled');
|
||||
|
||||
// Record that the guard path was taken; no SINK_REACHED marker is written.
|
||||
recordTrace('POST /api/exec', ['app.js:handleRequest', 'guard: FEATURE_ENABLE != 1']);
|
||||
recordCoverage('src/app.js', [5, 6, 7, 9, 10, 11]);
|
||||
})();
|
||||
38
bench/reachability-benchmark/cases/js/unsafe-eval/case.yaml
Normal file
38
bench/reachability-benchmark/cases/js/unsafe-eval/case.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
id: "js-unsafe-eval:001"
|
||||
language: js
|
||||
project: unsafe-eval
|
||||
version: "1.0.0"
|
||||
description: "Minimal handler with unsafe eval sink reachable via POST /api/exec"
|
||||
entrypoints:
|
||||
- "POST /api/exec"
|
||||
sinks:
|
||||
- id: "UnsafeEval::handleRequest"
|
||||
path: "src/app.js::handleRequest"
|
||||
kind: "process"
|
||||
location:
|
||||
file: src/app.js
|
||||
line: 12
|
||||
notes: "eval on user-controlled input"
|
||||
environment:
|
||||
os_image: "node:20-alpine"
|
||||
runtime:
|
||||
node: "20.11.0"
|
||||
source_date_epoch: 1730000000
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Unit test triggers eval sink with payload {code: '1+2'}"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/js-unsafe-eval.json"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "js-unsafe-eval:001"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/exec"
|
||||
route: "/api/exec"
|
||||
method: "POST"
|
||||
handler: "app.js::handleRequest"
|
||||
description: "Executes user-supplied code (unsafe eval)"
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "rb-case-unsafe-eval",
|
||||
"version": "1.0.0",
|
||||
"description": "Reachability benchmark case: unsafe eval in minimal JS handler",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "./tests/run-tests.sh"
|
||||
}
|
||||
}
|
||||
17
bench/reachability-benchmark/cases/js/unsafe-eval/src/app.js
Normal file
17
bench/reachability-benchmark/cases/js/unsafe-eval/src/app.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
// Minimal HTTP-like handler exposing an unsafe eval sink for reachability.
|
||||
// The handler is intentionally small to avoid external dependencies.
|
||||
function handleRequest(body) {
|
||||
const code = body && body.code;
|
||||
if (typeof code !== 'string') {
|
||||
return { status: 400, body: 'bad request' };
|
||||
}
|
||||
|
||||
// Dangerous: executes user-controlled code. The test drives this sink.
|
||||
// eslint-disable-next-line no-eval
|
||||
const result = eval(code);
|
||||
return { status: 200, body: String(result) };
|
||||
}
|
||||
|
||||
module.exports = { handleRequest };
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1730000000}
|
||||
export TZ=UTC
|
||||
export LC_ALL=C
|
||||
node test_reach.js
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { handleRequest } = require('../src/app');
|
||||
|
||||
const OUT_DIR = path.resolve(__dirname, '../outputs');
|
||||
const TRACE_DIR = path.join(OUT_DIR, 'traces');
|
||||
const COVERAGE_FILE = path.join(OUT_DIR, 'coverage.json');
|
||||
const TRACE_FILE = path.join(TRACE_DIR, 'traces.json');
|
||||
|
||||
function ensureDirs() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.mkdirSync(TRACE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function recordTrace(entry, pathNodes) {
|
||||
fs.writeFileSync(
|
||||
TRACE_FILE,
|
||||
JSON.stringify({
|
||||
entry,
|
||||
path: pathNodes,
|
||||
sink: 'UnsafeEval::handleRequest',
|
||||
notes: 'Test-driven dynamic trace'
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function recordCoverage(filePath, lines) {
|
||||
fs.writeFileSync(
|
||||
COVERAGE_FILE,
|
||||
JSON.stringify({
|
||||
files: {
|
||||
[filePath]: {
|
||||
lines_covered: lines,
|
||||
lines_total: 30
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
(function main() {
|
||||
ensureDirs();
|
||||
const payload = { code: '1 + 2' };
|
||||
const response = handleRequest(payload);
|
||||
assert.strictEqual(response.status, 200);
|
||||
assert.strictEqual(response.body, '3');
|
||||
|
||||
recordTrace('POST /api/exec', ['app.js:handleRequest', 'eval(code)']);
|
||||
recordCoverage('src/app.js', [5, 6, 7, 12, 15]);
|
||||
// Marker file proves sink executed
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'SINK_REACHED'), 'true');
|
||||
})();
|
||||
Reference in New Issue
Block a user