Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6310282
wip: very first taint impl
MaxAtoms May 3, 2026
3bae296
wip: very first dsl draft
MaxAtoms May 3, 2026
112f042
docs-fix: typo
MaxAtoms May 4, 2026
2612746
wip: add a very crude demo of conditional taints
MaxAtoms May 4, 2026
e12fe67
wip: a bit of clean-up
MaxAtoms May 5, 2026
5dd99c3
lint-fix: annotation
MaxAtoms May 5, 2026
78bc93f
feat: generic testing framework for abstract interpretation inference
OliverGerstl May 6, 2026
8ac2f43
wip: improve dsl and predefined analyses
MaxAtoms May 7, 2026
38ea1de
feat: add taint analysis to analyzer
MaxAtoms May 7, 2026
9ed75c3
feat: add taint analysis query
MaxAtoms May 7, 2026
08638f5
feat: adapt data frame shape inference tests to new test framework
OliverGerstl May 7, 2026
e6ba49f
feat: add generic test function for value inference
OliverGerstl May 11, 2026
ebb641e
Merge remote-tracking branch 'origin/main' into validity-prototype
MaxAtoms May 11, 2026
b1c05a8
build-fix: get rid of type errors
MaxAtoms May 11, 2026
717bab5
test: add test file
MaxAtoms May 11, 2026
954e515
Merge remote-tracking branch 'origin/2471-provide-a-generic-testing-f…
MaxAtoms May 11, 2026
9d141c3
build-fix: fix type
MaxAtoms May 12, 2026
f041d47
wip: draft of finite domain builder
MaxAtoms May 12, 2026
d51279e
feat-fix: leq of finite domain
MaxAtoms May 12, 2026
1586168
wip: add basic finite domain tests
MaxAtoms May 12, 2026
31b9135
wip: improved finite lattice
MaxAtoms May 18, 2026
843bbd3
tests: add a first set of taint analysis test cases
MaxAtoms May 20, 2026
c05bfc0
Merge remote-tracking branch 'origin/main' into validity-prototype
MaxAtoms May 22, 2026
b21aca5
wip: attempt to add type safety to analysis names
MaxAtoms May 22, 2026
bebfc7c
tests-fix: fix concretize and abstract cases
MaxAtoms May 22, 2026
1bd5e7c
refactor: some clean-up
MaxAtoms May 22, 2026
26ff15b
refactor: use Identifier helper object
MaxAtoms May 25, 2026
c4eac44
feat: simplify finite lattice def
MaxAtoms May 27, 2026
067f410
test-fix: add test call
MaxAtoms May 27, 2026
2727f19
feat-fix: type safety for predefined taint analyses
EagleoutIce May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/project/flowr-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { Tree } from 'web-tree-sitter';
import { normalizeTreeSitterTreeToAst } from '../r-bridge/lang-4.x/tree-sitter/tree-sitter-normalize';
import { TreeSitterExecutor } from '../r-bridge/lang-4.x/tree-sitter/tree-sitter-executor';
import type { CallGraph } from '../dataflow/graph/call-graph';
import { TaintAnalysis } from '../taint-analysis/builder/taint-analysis';

/**
* Extends the {@link ReadonlyFlowrAnalysisProvider} with methods that allow modifying the analyzer state.
Expand Down Expand Up @@ -137,6 +138,10 @@ export interface ReadonlyFlowrAnalysisProvider<Parser extends KnownParser = Know
* @param query - The list of queries.
*/
query<Types extends SupportedQueryTypes = SupportedQueryTypes>(query: Queries<Types>): Promise<QueryResults<Types>>;
/**
* Access the taint analysis API for the request.
*/
taint(): TaintAnalysis;
/**
* Run a search on the current analysis.
*/
Expand Down Expand Up @@ -313,6 +318,10 @@ export class FlowrAnalyzer<Parser extends KnownParser = KnownParser> implements
return runSearch(search, this);
}

public taint(): TaintAnalysis {
return new TaintAnalysis(this);
}

/**
* Close the parser if it was created by this builder. This is only required if you rely on an RShell/remote engine.
*/
Expand Down
32 changes: 32 additions & 0 deletions src/queries/catalog/taint-query/taint-query-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { log } from '../../../util/log';
import type { BasicQueryData } from '../../base-query-format';
import type { TaintQuery, TaintQueryResult } from './taint-query-format';

/**
* Executes the given taint queries using the provided analyzer.
*/
export async function executeTaintQuery({ analyzer }: BasicQueryData, queries: readonly TaintQuery[]): Promise<TaintQueryResult> {
const flattened = queries.flatMap(q => q.defs);

Check failure on line 9 in src/queries/catalog/taint-query/taint-query-executor.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Unsafe return of a value of type `any[]`

if(flattened.length == 0) {
log.warn('Missing taint query definition');
}

const start = Date.now();
const analysis = analyzer.taint();
for(const def of flattened) {
analysis.addPredefined(def);
}

const visitors = await analysis.run();
const results = new Map(
Array.from(visitors, ([k, v]) => [k, v.getEndState()])
);

return {
results: results,
'.meta': {
timing: Date.now() - start
},
};
}
125 changes: 125 additions & 0 deletions src/queries/catalog/taint-query/taint-query-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { BaseQueryFormat, BaseQueryResult } from '../../base-query-format';
import type { ReplOutput } from '../../../cli/repl/commands/repl-main';
import type { FlowrConfig } from '../../../config';
import type { ParsedQueryLine, QueryResults, SupportedQuery } from '../../query';
import Joi from 'joi';
import { executeTaintQuery } from './taint-query-executor';
import type { PredefinedTaintAnalysis } from '../../../taint-analysis/predefined/predefined';

Check failure on line 7 in src/queries/catalog/taint-query/taint-query-format.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../../../taint-analysis/predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?

Check failure on line 7 in src/queries/catalog/taint-query/taint-query-format.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../../../taint-analysis/predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?

Check failure on line 7 in src/queries/catalog/taint-query/taint-query-format.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../../../taint-analysis/predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?

Check failure on line 7 in src/queries/catalog/taint-query/taint-query-format.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../../../taint-analysis/predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?
import { predefinedTaintAnalyses } from '../../../taint-analysis/predefined/predefined';
import type { AnyAbstractDomain } from '../../../abstract-interpretation/domains/abstract-domain';
import { Bottom, BottomSymbol } from '../../../abstract-interpretation/domains/lattice';
import { bold } from '../../../util/text/ansi';
import { printAsMs } from '../../../util/text/time';
import type { CommandCompletions } from '../../../cli/repl/core';
import { fileProtocol } from '../../../r-bridge/retriever';
import type { AnyStateDomain } from '../../../abstract-interpretation/domains/state-domain-like';
import type { StateDomainLift } from '../../../abstract-interpretation/domains/state-abstract-domain';

export interface TaintQuery extends BaseQueryFormat {
readonly type: 'taint';
readonly defs: PredefinedTaintAnalysis[]
}

export interface TaintQueryResult extends BaseQueryResult {
readonly results: Map<string, AnyStateDomain>
}

const prefix = 'definitions:';

function taintQueryCompleter(line: readonly string[], startingNewArg: boolean, _config: FlowrConfig): CommandCompletions {
const prefixNotPresent = line.length == 0 || (line.length == 1 && line[0].length < prefix.length);
const notFinished = line.length == 1 && line[0].startsWith(prefix) && !startingNewArg;
const endOfOptions = line.length == 1 && startingNewArg || line.length == 2;

if(prefixNotPresent) {
return { completions: [`${prefix}`] };
} else if(endOfOptions) {
return { completions: [fileProtocol] };
} else if(notFinished) {
const withoutPrefix = line[0].slice(prefix.length);
const used = withoutPrefix.split(',').map(r => r.trim());
const all = Object.keys(predefinedTaintAnalyses);
const unused = all.filter(r => !used.includes(r));
const last = used[used.length - 1];
const lastUnfinished = !all.includes(last);

if(lastUnfinished) {
// Return all strings that have not been added yet
return { completions: unused, argumentPart: last };
} else if(unused.length > 0) {
// Add a comma, if the current last string is complete
return { completions: [','], argumentPart: '' };
} else {
// All strings are used, complete with a space
return { completions: [' '], argumentPart: '' };
}
}
return { completions: [] };
}

function defsInInput(defsPart: readonly string[]): { valid: (PredefinedTaintAnalysis)[], invalid: string[] } {
return defsPart
.reduce((acc, name) => {
name = name.trim();
if(name in predefinedTaintAnalyses) {
acc.valid.push(name as PredefinedTaintAnalysis);
} else {
acc.invalid.push(name);
}
return acc;
}, { valid: [] as (PredefinedTaintAnalysis)[], invalid: [] as string[] });
}

function taintQueryLineParser(output: ReplOutput, line: readonly string[], _config: FlowrConfig): ParsedQueryLine<'taint'> {
let defs: PredefinedTaintAnalysis[] = [];
let input: string | undefined = undefined;
if(line.length > 0 && line[0].startsWith(prefix)) {
const defsPart = line[0].slice(prefix.length).split(',');
const parseResult = defsInInput(defsPart);
if(parseResult.invalid.length > 0) {
output.stderr(`Unknown taint definition names: ${parseResult.invalid.map(r => bold(r, output.formatter)).join(', ')}`
+`\nKnown taint definitions are: ${Object.keys(predefinedTaintAnalyses).map(r => bold(r, output.formatter)).join(', ')}`);
}
defs = parseResult.valid;
input = line[1];
} else if(line.length > 0) {
input = line[0];
}
return { query: [{ type: 'taint', defs: defs }], rCode: input } ;
}

export const TaintQueryDefinition = {
executor: executeTaintQuery,
asciiSummarizer: (formatter, _analyzer, queryResults, result) => {
const out = queryResults as QueryResults<'taint'>['taint'];
const state = out.results.entries().toArray();
result.push(`Query: ${bold('taint', formatter)} (${printAsMs(out['.meta'].timing, 0)})`);

for(const [name, domains] of state) {
result.push(` ╰ **${name}**:`);
const lift = domains.value as StateDomainLift<AnyAbstractDomain>;
if(lift === Bottom) {
result.push(` ╰ state: ${BottomSymbol}`);
return true;
}

result.push(...lift.entries().take(20).map(([key, domain]) => {
return ` ╰ ${key}: ${domain?.toString()}`;
}));

if(result.length > 20) {
result.push(' ╰ ... (see JSON)');
}
}

return true;
},
completer: taintQueryCompleter,
fromLine: taintQueryLineParser,
schema: Joi.object({
type: Joi.string().valid('taint').required().description('The type of the query.'),
defs: Joi.array().description('The taint analyses to run.')
}).description('The taint query conducts taint analyses and returns their results.'),
flattenInvolvedNodes: () => []
} as const satisfies SupportedQuery<'taint'>;

4 changes: 4 additions & 0 deletions src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ import {
} from './catalog/input-sources-query/input-sources-query-format';
import type { ProvenanceQuery } from './catalog/provenance-query/provenance-query-format';
import { ProvenanceQueryDefinition } from './catalog/provenance-query/provenance-query-format';
import type { TaintQuery } from './catalog/taint-query/taint-query-format';
import { TaintQueryDefinition } from './catalog/taint-query/taint-query-format';

/**
* These are all queries that can be executed from within flowR
Expand Down Expand Up @@ -115,6 +117,7 @@ export type Query = CallContextQuery
| LinterQuery
| ProvenanceQuery
| InputSourcesQuery
| TaintQuery
;

export type QueryArgumentsWithType<QueryType extends BaseQueryFormat['type']> = Query & { type: QueryType };
Expand Down Expand Up @@ -165,6 +168,7 @@ export const SupportedQueries = {
'does-call': DoesCallQueryDefinition,
'dataflow-lens': DataflowLensQueryDefinition,
'df-shape': DfShapeQueryDefinition,
'taint': TaintQueryDefinition,
'files': FilesQueryDefinition,
'id-map': IdMapQueryDefinition,
'normalized-ast': NormalizedAstQueryDefinition,
Expand Down
79 changes: 79 additions & 0 deletions src/taint-analysis/builder/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Bottom, Top } from '../../abstract-interpretation/domains/lattice';
import type { FiniteLatticeConfig } from '../finite-domain';
import { FiniteDomain } from '../finite-domain';
import { guard } from '../../util/assert';

export class FiniteDomainBuilder<Element extends Top | Bottom | symbol, Top extends symbol, Bottom extends symbol> {
private readonly elements: Set<Element> = new Set();
private readonly leqMap: Map<Element, Set<Element>> = new Map();
private _top: Top | undefined;
private _bottom: Bottom | undefined;

private addElements(...elements: Element[]) {
for(const element of elements) {
if(!this.elements.has(element)) {
this.elements.add(element);
}
if(!this.leqMap.has(element)) {
this.leqMap.set(element, new Set());
}
}
}

setTop(element: Top): this {
this._top = element;
this.addElements(element as Element);
return this;
}

setBottom(element: Bottom): this {
this._bottom = element;
this.addElements(element as Element);
return this;
}

addLeqOrder(from: Element, to: Element | Element[]): this {
this.addElements(from, ...(Array.isArray(to) ? to : [to]));


if(!this.elements.has(from)) {
throw new Error(`Source element not registered: ${String(from)}`);
}
if(!Array.isArray(to)) {
to = [to];
}

const successors = this.leqMap.get(from);
guard(successors, 'Internal error: A successor set should always exist');

for(const t of to) {
if(!this.elements.has(t)) {
throw new Error(`Target element not registered: ${String(t)}`);
}

successors.add(t);
}
return this;
}

private buildConfig(): FiniteLatticeConfig<Element, Top, Bottom> {
if(!this._top) {
this.setTop(Top as Top);
}

if(!this._bottom) {
this.setBottom(Bottom as Bottom);
}

return {
top: this._top as Top,
bottom: this._bottom as Bottom,
elements: new Set(this.elements),
leq: new Map(this.leqMap),
};
}

build(initialElement?: Element): FiniteDomain<Element, Top, Bottom> {
return new FiniteDomain(initialElement ?? this._top as Element, this.buildConfig());
}
}
35 changes: 35 additions & 0 deletions src/taint-analysis/builder/taint-analysis-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { AnyAbstractDomain } from '../../abstract-interpretation/domains/abstract-domain';
import type { TaintMapper } from '../function-mapper';

export type TaintAnalysisName<Definition> = Definition extends TaintAnalysisDefinition<infer Name, infer _Domain> ? Name : never;

/**
* Fluent builder class for defining new taint analyses.
*/
export class TaintAnalysisDefinition<Name extends string, Domain extends AnyAbstractDomain = AnyAbstractDomain> {
public readonly domain: Domain;
public mapper: TaintMapper<Domain> = [];
public name: Name;

private msg: string | undefined;

constructor(name: Name, domain: Domain) {
this.name = name;
this.domain = domain;
}

public through(fnMapping: TaintMapper<Domain>): this {
this.mapper.push(...fnMapping);
return this;
}

public to(fnMapping: TaintMapper<Domain>): this {
this.mapper.push(...fnMapping);
return this;
}

public report(msg: string): this {
this.msg = msg;
return this;
}
}
48 changes: 48 additions & 0 deletions src/taint-analysis/builder/taint-analysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer';
import type { TaintAnalysisDefinition } from './taint-analysis-definition';
import type { PredefinedTaintAnalysis } from '../predefined/predefined';

Check failure on line 3 in src/taint-analysis/builder/taint-analysis.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?

Check failure on line 3 in src/taint-analysis/builder/taint-analysis.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?

Check failure on line 3 in src/taint-analysis/builder/taint-analysis.ts

View workflow job for this annotation

GitHub Actions / ⚗️ Test Suite (coverage)

'"../predefined/predefined"' has no exported member named 'PredefinedTaintAnalysis'. Did you mean 'predefinedTaintAnalyses'?
import { predefinedTaintAnalyses } from '../predefined/predefined';
import { TaintInferenceVisitor } from '../taint-visitor';
import type { AnyAbstractDomain } from '../../abstract-interpretation/domains/abstract-domain';

/**
* Fluent builder class for conducting taint analyses.
* Please prefer using the {@link FlowrAnalyzer.taint} method to create a taint analysis.
*/
export class TaintAnalysis<Defs extends readonly string[] = []> {
private readonly analyzer: ReadonlyFlowrAnalysisProvider;
private readonly defs: TaintAnalysisDefinition<Defs[number]>[] = [];

constructor(analyzer: ReadonlyFlowrAnalysisProvider) {
this.analyzer = analyzer;
}

public addPredefined<Name extends PredefinedTaintAnalysis>(name: Name): TaintAnalysis<readonly [...Defs, Name]> {
this.defs.push(predefinedTaintAnalyses[name]);
return this as unknown as TaintAnalysis<readonly [...Defs, Name]>;
}

public add<Name extends string>(def: TaintAnalysisDefinition<Name>): TaintAnalysis<readonly [...Defs, Name]> {
this.defs.push(def);
return this as unknown as TaintAnalysis<readonly [...Defs, Name]>;
}

/**
* Run one or multiple taint analyses.
* Note: Requires a prior call to {@link TaintAnalysis.add} or {@link TaintAnalysis.addPredefined} to add at least one taint analysis.
*/
public async run(): Promise<Map<Defs[number], TaintInferenceVisitor<AnyAbstractDomain>>> {
const results: Map<Defs[number], TaintInferenceVisitor<AnyAbstractDomain>> = new Map();
for(const def of this.defs) {
const visitor = new TaintInferenceVisitor(def.domain, def.mapper, {
controlFlow: await this.analyzer.controlflow(),
ctx: this.analyzer.inspectContext(),
dfg: (await this.analyzer.dataflow()).graph,
normalizedAst: await this.analyzer.normalize()
});
visitor.start();
results.set(def.name, visitor);
}
return results;
}
}
Loading
Loading