withAuditFlow
Statically audits a flow for taint violations — checks that no "sink" step (e.g. an outbound HTTP call) can be reached after a "source" step (e.g. a PII-fetching step), without executing the flow at all.
Setup
typescript
import { FlowBuilder } from "flowneer";
import { withAuditFlow } from "flowneer/plugins/compliance";
const AppFlow = FlowBuilder.extend([withAuditFlow]);Usage
typescript
import { withAuditFlow } from "flowneer/plugins/compliance";
import type { TaintRule } from "flowneer/plugins/compliance";
const flow = new AppFlow<State>()
.then(fetchUser, { label: "pii:fetchUser" })
.then(enrichProfile, { label: "pii:enrich" })
.then(callAnalytics, { label: "external:analytics" })
.then(saveResult);
const rules: TaintRule[] = [
{
source: ["pii:*"],
sink: (meta) => meta.label?.startsWith("external:") ?? false,
message: "PII must not reach external endpoints",
onViolation: "throw",
},
];
const report = flow.auditFlow(rules);
if (!report.passed) {
for (const v of report.violations) {
console.error(
`Rule violation: step "${v.source.label}" (index ${v.source.index}) ` +
`can reach sink "${v.sink.label}" (index ${v.sink.index})`,
);
}
}TaintRule
typescript
interface TaintRule {
/** Steps that produce sensitive data. Matched via StepFilter. */
source: StepFilter;
/** Steps that send data outbound. Matched via StepFilter. */
sink: StepFilter;
/** Human-readable description, included in violation messages. */
message?: string;
/** What to do at runtime via withRuntimeCompliance. Defaults to "throw". */
onViolation?: "throw" | "warn" | "record";
}ComplianceReport
typescript
interface ComplianceReport {
passed: boolean;
violations: ComplianceViolation[];
}
interface ComplianceViolation {
rule: TaintRule;
source: { index: number; label?: string };
sink: { index: number; label?: string };
}StepFilter patterns
source and sink accept any StepFilter: a string array (exact labels or glob patterns) or a predicate function.
typescript
// Exact labels
{ source: ["fetchUser"], sink: ["sendToAnalytics"] }
// Glob — matches "pii:user", "pii:address", etc.
{ source: ["pii:*"], sink: ["external:*"] }
// Predicate
{
source: (meta) => meta.label?.startsWith("pii:") ?? false,
sink: (meta) => meta.label?.startsWith("external:") ?? false,
}Steps without a label are never matched by string patterns.
How It Works
auditFlow() walks the compiled steps[] array recursively (including nested .loop() bodies and .batch() processors). For each rule it collects all source and sink positions, then flags any sink that appears at a higher index than a source.
Nothing is executed — the analysis is purely structural and synchronous.
Notes
- The analysis is conservative by design: it reports any structural path from source to sink regardless of runtime conditions. A sink that could only be reached when a branch is false is still reported.
- For runtime enforcement (checking actual shared state values), use
withRuntimeCompliance. - Combine both for defence-in-depth:
auditFlow()at startup to catch structural issues,withRuntimeComplianceduring execution to catch data-level issues.