mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* feat(taint): add conservative Java source model * fix(taint): preserve Java import provenance * chore: retry CI after network timeout --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
/**
|
|
* U1 (#2084 M4) — per-function taint summary harvest.
|
|
*
|
|
* Fixtures parse REAL TypeScript through the shared CFG/import harness, so the
|
|
* harvester consumes the exact `FunctionCfg` / `FunctionDefUse` /
|
|
* `FunctionSiteMatches` structures the pipeline produces. The four summary
|
|
* edge categories are asserted directly: param→return, param→callee-arg,
|
|
* param→sink, source→return.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { cfgOf, importsFor } from '../../helpers/ts-cfg-harness.js';
|
|
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
|
|
import { computeReachingDefs } from '../../../src/core/ingestion/cfg/reaching-defs.js';
|
|
import {
|
|
buildTaintImportIndex,
|
|
matchFunctionSites,
|
|
} from '../../../src/core/ingestion/taint/match.js';
|
|
import type { SourceSinkSanitizerSpec } from '../../../src/core/ingestion/taint/source-sink-config.js';
|
|
import { harvestFunctionSummary } from '../../../src/core/ingestion/taint/summary-harvest.js';
|
|
|
|
const SPEC: SourceSinkSanitizerSpec = {
|
|
sources: [{ kind: 'remote-input', objects: ['req'], properties: ['body', 'query', 'params'] }],
|
|
sinks: [
|
|
{ name: 'exec', kind: 'command-injection', args: [0], global: true },
|
|
{ name: 'query', kind: 'sql-injection', args: [0], anyReceiver: true },
|
|
],
|
|
sanitizers: [{ name: 'escape', neutralizes: ['command-injection'], global: true }],
|
|
};
|
|
|
|
const CALL_RESULT_SOURCE_SPEC: SourceSinkSanitizerSpec = {
|
|
sources: [
|
|
{
|
|
type: 'call-result',
|
|
kind: 'remote-input',
|
|
receivers: ['request'],
|
|
methods: ['getParameter'],
|
|
},
|
|
],
|
|
sinks: [],
|
|
sanitizers: [],
|
|
};
|
|
|
|
function harvest(code: string, spec: SourceSinkSanitizerSpec = SPEC, fnIndex = 0) {
|
|
const cfg: FunctionCfg = cfgOf(code, fnIndex);
|
|
const defUse = computeReachingDefs(cfg);
|
|
const matches = matchFunctionSites(cfg, spec, buildTaintImportIndex(importsFor(code)));
|
|
return harvestFunctionSummary(cfg, defUse, matches).facts;
|
|
}
|
|
|
|
describe('harvestFunctionSummary — param→return', () => {
|
|
it('records a param flowing straight to return', () => {
|
|
const f = harvest(`function f(x: string) { return x; }`);
|
|
expect(f.paramCount).toBe(1);
|
|
expect(f.paramToReturn).toEqual([{ param: 0 }]);
|
|
});
|
|
|
|
it('records a param returned through a local assignment', () => {
|
|
const f = harvest(`function f(x: string) { const y = x; return y; }`);
|
|
expect(f.paramToReturn).toEqual([{ param: 0 }]);
|
|
});
|
|
|
|
it('records receiver-TITO return (x.trim())', () => {
|
|
const f = harvest(`function f(x: string) { return x.trim(); }`);
|
|
expect(f.paramToReturn.map((r) => r.param)).toContain(0);
|
|
});
|
|
|
|
it('does not record an unrelated param', () => {
|
|
const f = harvest(`function f(x: string, y: string) { return x; }`);
|
|
expect(f.paramToReturn.map((r) => r.param)).toEqual([0]);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — param→callee-arg', () => {
|
|
it('records a param flowing into a callee argument', () => {
|
|
const f = harvest(`function f(x: string) { helper(x); }`);
|
|
const ca = f.paramToCallArg;
|
|
expect(ca.length).toBeGreaterThanOrEqual(1);
|
|
expect(ca.some((c) => c.param === 0 && c.argIndex === 0 && c.calleeName === 'helper')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('records the correct argument index', () => {
|
|
const f = harvest(`function f(x: string) { helper(a, x); }`);
|
|
expect(f.paramToCallArg.some((c) => c.param === 0 && c.argIndex === 1)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — param→sink', () => {
|
|
it('records a param reaching a modelled sink', () => {
|
|
const f = harvest(`function f(x: string) { exec(x); }`);
|
|
expect(f.paramToSink).toEqual([{ param: 0, sinkKind: 'command-injection' }]);
|
|
});
|
|
|
|
it('a sanitizer neutralises the matching sink kind', () => {
|
|
const f = harvest(`function f(x: string) { const y = escape(x); exec(y); }`);
|
|
// escape neutralises command-injection on the path to exec → no param→sink.
|
|
expect(f.paramToSink).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — call-arg sanitizer exclusions (#2084 review P1-2)', () => {
|
|
it('carries the neutralized kind onto a param→callee-arg edge', () => {
|
|
// x → escape(x) → y → helper(y): the call-arg edge to the user fn `helper`
|
|
// records that command-injection was neutralised on the path.
|
|
const f = harvest(`function f(x: string) { const y = escape(x); helper(y); }`);
|
|
const edge = f.paramToCallArg.find((c) => c.calleeName === 'helper');
|
|
if (edge === undefined) throw new Error('expected helper call-arg edge');
|
|
expect(edge.neutralized).toEqual(['command-injection']);
|
|
});
|
|
|
|
it('records no neutralized when the param reaches the call directly', () => {
|
|
const f = harvest(`function f(x: string) { helper(x); }`);
|
|
const edge = f.paramToCallArg.find((c) => c.calleeName === 'helper');
|
|
if (edge === undefined) throw new Error('expected helper call-arg edge');
|
|
expect(edge.neutralized).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — source→callee-arg (fixpoint seed)', () => {
|
|
it('records a source passed directly into a callee argument', () => {
|
|
const f = harvest(`function f() { runIt(req.body); }`);
|
|
expect(f.sourceToCallArg.some((s) => s.argIndex === 0 && s.calleeName === 'runIt')).toBe(true);
|
|
});
|
|
|
|
it('records a source passed via a local into a callee argument', () => {
|
|
const f = harvest(`function f() { const u = req.body; runIt(u); }`);
|
|
expect(f.sourceToCallArg.some((s) => s.calleeName === 'runIt')).toBe(true);
|
|
});
|
|
|
|
it('records an assigned call-result source passed via a local into a callee argument', () => {
|
|
const f = harvest(
|
|
`function f(request: { getParameter(name: string): string }) {
|
|
const u = request.getParameter('path');
|
|
runIt(u);
|
|
}`,
|
|
CALL_RESULT_SOURCE_SPEC,
|
|
);
|
|
expect(f.sourceToCallArg).toEqual([
|
|
{ sourceKind: 'remote-input', callLine: 3, argIndex: 0, calleeName: 'runIt' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — call-result seeds (#2084 review P1-1)', () => {
|
|
it('records a generative call result reaching a sink via a local', () => {
|
|
const f = harvest(`function f() { const t = getInput(); exec(t); }`);
|
|
expect(f.callResults.some((cr) => cr.calleeName === 'getInput' && cr.dest.to === 'sink')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('records a call result flowing into another callee arg', () => {
|
|
const f = harvest(`function f() { const t = getInput(); forward(t); }`);
|
|
expect(
|
|
f.callResults.some(
|
|
(cr) =>
|
|
cr.calleeName === 'getInput' &&
|
|
cr.dest.to === 'callArg' &&
|
|
cr.dest.toCallee === 'forward',
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('records a bare `return getInput()` as a call result → return', () => {
|
|
const f = harvest(`function f() { return getInput(); }`);
|
|
expect(
|
|
f.callResults.some((cr) => cr.calleeName === 'getInput' && cr.dest.to === 'return'),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('does not record call results for sink/sanitizer calls', () => {
|
|
const f = harvest(`function f(x: string) { exec(escape(x)); }`);
|
|
// exec is a sink, escape is a sanitizer — neither is a user-fn call result.
|
|
expect(f.callResults.some((cr) => cr.calleeName === 'exec' || cr.calleeName === 'escape')).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — source→return', () => {
|
|
it('records a generated source returned directly', () => {
|
|
const f = harvest(`function f() { return req.body; }`);
|
|
expect(f.sourceToReturn).toEqual([{ sourceKind: 'remote-input' }]);
|
|
});
|
|
|
|
it('records a generated source returned via a local', () => {
|
|
const f = harvest(`function f() { const u = req.body; return u; }`);
|
|
expect(f.sourceToReturn).toEqual([{ sourceKind: 'remote-input' }]);
|
|
});
|
|
|
|
it('records an assigned call-result source returned via a local', () => {
|
|
const f = harvest(
|
|
`function f(request: { getParameter(name: string): string }) {
|
|
const u = request.getParameter('path');
|
|
return u;
|
|
}`,
|
|
CALL_RESULT_SOURCE_SPEC,
|
|
);
|
|
expect(f.sourceToReturn).toEqual([{ sourceKind: 'remote-input' }]);
|
|
});
|
|
|
|
it('is empty when no source is present', () => {
|
|
const f = harvest(`function f(x: string) { return x; }`);
|
|
expect(f.sourceToReturn).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — documented limitations', () => {
|
|
it('all-simple params map to their formal argument position', () => {
|
|
const f = harvest(`function f(a: string, b: string) { exec(b); }`);
|
|
// `b` is formal param 1 — the index the interproc solver joins against.
|
|
expect(f.paramToSink).toEqual([{ param: 1, sinkKind: 'command-injection' }]);
|
|
});
|
|
|
|
it('destructured param before a simple param shifts the index (known FN, pinned)', () => {
|
|
// `function f([a, b], x)` — formal positions are [a,b]=0, x=1. The harvest
|
|
// assigns by binding ordinal (a=0, b=1, x=2), so x's port is 2, not the
|
|
// formal 1 the solver joins against → documented cross-function FN. Pinned
|
|
// so the behaviour is a known boundary, not a silent surprise; the proper
|
|
// fix (formal-param index from the worker) is deferred.
|
|
const f = harvest(`function f([a, b]: string[], x: string) { exec(x); }`);
|
|
const xSink = f.paramToSink.find((s) => s.sinkKind === 'command-injection');
|
|
if (xSink === undefined) throw new Error('expected command-injection param sink');
|
|
// Current (limited) behaviour: ordinal index 2, NOT the formal index 1.
|
|
expect(xSink.param).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('harvestFunctionSummary — edges & gaps', () => {
|
|
it('empty summary for a param-less, site-less function', () => {
|
|
const f = harvest(`function f() { const a = 1; return a; }`);
|
|
expect(f.paramToReturn).toEqual([]);
|
|
expect(f.paramToCallArg).toEqual([]);
|
|
expect(f.paramToSink).toEqual([]);
|
|
expect(f.sourceToReturn).toEqual([]);
|
|
});
|
|
|
|
it('reports a coverage gap when reaching-defs is not computed', () => {
|
|
// A hand-built CFG with no bindings → reaching-defs returns no-facts.
|
|
const cfg = cfgOf(`function f(x: string) { return x; }`);
|
|
const bare = { ...cfg, bindings: undefined } as FunctionCfg;
|
|
const defUse = computeReachingDefs(bare);
|
|
const matches = matchFunctionSites(bare, SPEC, buildTaintImportIndex([]));
|
|
const r = harvestFunctionSummary(bare, defUse, matches);
|
|
expect(r.status).toBe('coverage-gap');
|
|
});
|
|
});
|