Repository navigation
Releases: v0lka/flowsh
Release list
v0.6.0
A policy-and-privilege release. A new optional public policy package turns a
report into an allow/review/deny decision, and the bash binder learns to
elevate a command through the sudo wrapper.
Added
- Report policy package. The optional public package
github.com/v0lka/flowsh/policyevaluates anapi.Reportunder trusted,
caller-owned Go configuration and returns anAllow,RevieworDeny
decision with deterministic, sorted reason codes
(ADR-0017).Newvalidates
the configuration and copies its maps, defaultingUnknowntoDeny,
ConservativetoReview,ExfiltrationtoDeny,CradletoReview
andIngesttoAllow, with emptyEffects/Gradesmaps;Evaluate
validates the report's tool name/version, schema version, canonical language
and grade before applying rules, so a missing, invalid or incompatible report
denies withinvalid_report, an otherwise-valid report whoseCovered()is
false denies withuncovered_report, and a nil or zero policy denies with
invalid_policy. All matching restrictions combine with
Deny > Review > Allow; unknown (Top || HasTop()) and conservative
analysis always require Review or Deny, and an Allow rule never overrides
another restriction. Decisions carry only the stable signal names
(unknown,conservative,exfiltration,cradle,ingest),
effect:<kind>andgrade:<grade>, never analysed text. The package depends
only onapiandengine, performs no I/O or command execution, and leaves
analysis, scoring, the CLI report and the report contract unchanged; it amends
ADR-0010's framing ofapias the sole public surface whileapiremains the
analysis entry point. A newspecs/domains/policy.mdrecords the contract,
anddocs/embedding.md,README.md,SECURITY.md,
specs/architecture/layers.mdandspecs/INDEX.mdname the decision layer. - sudo wrapper elevation (refined -u). The bash frontend now carries each
peeled wrapper's option words and source position intobind.Call
(Wrapper{Name, Options, Pos}— a source-breaking type change for code that
importedbinddirectly and readWrappersas[]string; the serialized
report contract is unchanged), and the binder runs a wrapper-elevation pass
before resolving the inner command: a wrapper whose knowledge-base record
declares a PrivEsc parameter (sudo, new inkb/data/security.yaml) binds
its own command line viaparseArgsand elevates by the refined -u doctrine —
no-u/--user, or a literal value naming root (root, or the numeric
spellings0,00,#0), yields a certain (Direct)PrivEsctargeting
root; a literal non-root target (sudo -u nobody …,sudo -u '#1000' …)
yields no elevation plus an explanatory note; a missing, dynamic, repeated
(sudo -u a -u b) or option-shaped value keeps the certain elevation over an
unresolved (⊤) target user without touchingResult.Conservative. A line
that names nothing to run (the inert flag-only formssudo -l,-v,-k,
-h,--version,--help,--list, … — including-l/--listwith a
command word, which sudo only lists permissions for, and the print-and-exit
--version/--help;-h/-v/-Kare likewise inert with a command word,
whereas-kwith a command still runs it, only invalidating the cached
credentials first) neither elevates, nor claims a de-elevation, nor
binds the words that follow: the report degrades to ⊤ instead.
-g/--groupnever decides nor elevates: a bare-gruns the command as
the invoking user (man sudo), and the unmodelled group membership is named by
a note. Unrecognized wrapper flags surface in the notes, and
sudo sudo cmdreports exactly one elevation. Every elevation is justified
in the why-trace by the wrapper's own tokens (command(sudo)at the
wrapper's position,flag(-u),operand(root), rulepriv.escalate).
The conformance corpus gains a sudo bundle (testdata/corpus/sudo_bash.json;
the latency baseline is re-pinned at 359 cases), and the accepted precision
trade-offs — the flag-only ⊤ verdict, the unpeeled path-qualified wrapper,
group escalation,doas/pkexecand the wrapper-less PowerShell path — are
recorded in SECURITY.md. The elevation also covers the call shapes that
never reach the ordinary binding flow: a wrapper that consumed the whole
command line (sudo -iruns a root shell with no inner command), a wrapper
over a dynamically-named command (sudo $X), over a code-execution sink
(sudo sh -c '…'), over a code-executing builtin (sudo eval '…') — the
interpreter routes all four through the resolver seam (elevateWrappers),
folding a result only when it carries elevation evidence, so a wrapper over
an effect-free line stays benign; when it does carry evidence, the report
gainsPrivEscalongside the interpreter's ⊤, and a ⊤ the interpreter
already derived is not duplicated by the binder's own.
Fixed
- Policy coverage diagnostics (2026-10 code review). The coverage gate is
applied to the validated report as its own step instead of being folded into
the report-validation check, so an otherwise-valid report with no effects and
neither theTopnor theConservativeflag — Bashtrueand:— denies
with the distinctuncovered_reportcode rather than being reported as
malformed. Validation still precedes coverage, so an invalid report always
usesinvalid_report. - Review hardening of the sudo elevation (2026-10 code review).
sudo -u '#0' …(and the00spelling) is classified numerically instead of
falling into a false "not root" de-elevation note;#1000is classified by
the same numeric reading and keeps its correct de-elevation; a repeated
-u/--userno longer resolves by the first match — the elevation
stays certain over an unresolved target with an ambiguity note; an option
token glued after an unsatisfied dynamic-u(sudo -u $U -g root …) is
never read as the target user;sudo -u"$U" cmd/sudo -u$U cmdkeep the
same -u semantics as the separated spelling (the recorded unsatisfied
option) instead of reporting a confident root escalation; and a
path-qualified/usr/bin/sudo …degrades to ⊤ like the unpeeled spelling it
is, instead of the knowledge-base record's narrow PrivEsc-only binding that
silently dropped the wrapped command's own effects. Under a download-cradle
flow (curl … | sudo $X) the report now carries a single ⊤CodeExecrow.
Inertness is decided at the privileged wrapper's own position in the chain,
not chain-wide: a wrapper above it execs the privileged wrapper, not the
other way round, soenv sudo -k/nohup sudo -k— andsudo sudo -k—
no longer reportPrivEscwith a "the command still runs" note for a line
that executes nothing;sudo env FOO=1keeps its elevation (the inner
wrapper is a process sudo execs), andbind's package documentation now
names both deliberate empty-result shapes of a directBindcall. A
trailing bare-u/--userwith no value on an inert line carries its own
explanatory note (real sudo usage-errors on the missing argument) instead
of staying silent.
Changed
- CI. The
golangci-lintGitHub Action is pinned to a version compatible
with Go 1.27.2 (.github/workflows/ci.yml).
v0.5.0
A corpus-and-recall release. An external balanced sample joins the conformance
corpus, and the four recall gaps it surfaced while screening the pool are
closed so the sample passes with zero silent misses.
Added
- External balanced corpus sample. A balanced sample (26+26 bash, 54+54
PowerShell) drawn from public labeled datasets under permissive licenses
joinstestdata/corpus/(external_bash.json,external_posh.json), with
the newexternal_malicious/external_benigngroups gated by the same
invariants as the rest of the corpus: no silent misses on the malicious side
and no ⊤ on the benign side. A PowerShell case may now set"windows": true
to run under the Windows-native provider profile, since registry effects are
host-gated by design. The latency baseline is re-pinned at 348 cases and the
domain specs are updated accordingly.
Fixed
- Four recall gaps closed so the external sample passes with zero misses:
Write-Output— and a top-level string-literal statement, which the parser
now normalizes toWrite-Output— carriesStdiowhen the pipeline is
unconsumed; a redirection suppresses it.Get-Command <name>andGet-Module -ListAvailablelower toFSRead
probes of the command path / module directories.- bash
jobsis no longer a shell-only builtin; the KB now describes its
stdout job-table output asStdio.
v0.4.0
A knowledge-base and binding release. It rolls up the changes made since
v0.3.2, whose full detail is recorded in the v0.3.3 and v0.3.4 entries
below.
Added
- Expanded ecosystem coverage in the knowledge base.
kb/data/toolchain.yaml
andkb/data/linting.yamlgain 114 commands across the Go, Node.js, Python,
JVM, Rust and Zig toolchains — with eleven class-C removals in
kb/data/destructive.yamland dedicatedgosubcommand specs in
kb/data/pkgmgr.yaml— so their everyday build, test and lint loops bind
deterministically instead of degrading to ⊤. The common-binary inventory gate
grows to 116 entries and the conformance corpus gains eighteen cross-ecosystem
benign cases. - Package-runner and project-local bin-path resolution. The binder resolves
npx/bunxand the./node_modules/.bin/<bin>path forms to the bare binary
and binds the resolved binary's own knowledge-base signature, so the everyday
JS-stack verification loop (npx vitest run …,npx tsc -b,
./node_modules/.bin/vitest run) binds deterministically instead of degrading
to ⊤. The fail-closed shapes — an operand outside the knowledge base, a
dynamic operand, a runner that names no operand,-c/--calland a
code-execution interpreter operand — are unchanged. The comparable form
(commandCalls[].resolved) reports the knowledge-base command the binder
resolved to, so every spelling of one binary shares one identity (npx mvnw,
./mvnwandmvnwall reportresolved: "mvn").
Changed
- Documentation. The command→dialect map in
specs/domains/knowledge-base.mdnow lists the complete inventory of every
command in the knowledge base, and thetoolchain.yaml/linting.yaml
descriptions name the expanded ecosystems.
v0.3.2
Fixed
- Knowledge-base corrections. A service or operation selector is now
valueFrom: envrather thanargs, so a subcommand word (kubectl get pods,
aws s3 ls) can no longer become a phantom egress target while an address
written on the command line still lowers concretely; output and selector
parameters (--json,-o,--output) becomeStdioinstead of a fabricated
file read; and the incorrect effect directions of theredis-cliandpsql
flags are fixed. - Binder download handling. The binder synthesises wget's implicit download
sink once per URL operand, folds-P/--directory-prefixinto the written
path, treats a-output value as standard output rather than a file named
-, and names curl-O's local file after the URL. - Flow-evidence isolation.
Report.Normalizemerges on
(kind, mode, netFlow), so the flow evidence of one effect cannot bleed onto
another's targets; the flow detectors now document their deliberate
many-to-one over-approximation. - bash frontend. Matches a code-execution sink by basename and through
env, and treats only network-labelled provenance as network content rather
than every command substitution. - PowerShell frontend. Lowers control-flow headers, fixes condition
truthiness and splat/member/index targets, expands literalforeachranges,
and closes the rest of the round-two code-review findings.
Changed
- Aligned the SECURITY.md threat model, the README report-field table (effects
merge by kind, mode and network-flow role) and the CONTRIBUTING corpus and
knowledge-base inventories (170 cases across seven documents; 23 reviewed
kb/data/*.yamldocuments) with the code, and added
ADR-0015 (per-host
latency reference). - Raised the knowledge-base load-latency budget to 150 ms after the tagged-commit
release runner (the sameubuntu-latestthat passes in CI) measured a worst
average of 52.3 ms — above the earlier 50 ms ceiling — and recorded the
decision in
ADR-0016.
v0.3.1
Fixed
- PowerShell download-cradle flow. The PowerShell frontend now establishes
the network→code-execution flow through a pipeline, so a fetch piped into a
code-execution sink (Invoke-WebRequest … | Invoke-Expression,curl … | iex)
is reported as a cradle flow exactly like its bash counterpart. Pipeline
stages now carry a shared group id so the lowerer can key the flow on the
value flow between stages — never on the mere co-occurrence of aNetEgress
and a code-execution sink — and the sinkCodeExecis marked with the
additivenetFlow: cradlerole.
v0.3.0
Added
-
Network data-flow fields (
flowsh/v3,effect-ir/v2). The report now
asserts two network data flows so a consumer can key on the flow rather than
infer it from the co-occurrence of aNetEgressand a sink:
score.cradleFlows(network content reaching code execution —curl … | sh,
source <(curl …),sh -c "$(curl …)"; a download-then-execute chain is not
currently asserted, because no frontend establishes that flow) and
score.ingestFlows(a download client writing fetched content to a file —
curl -o/-O, wget default/-O). The sink is marked by the additive
effect-levelnetFlowrole (cradleon aCodeExec,ingeston an
FSWrite), which bumps the effect IR toeffect-ir/v2; a program with no
network flow serialises byte-for-byte as before.DetectCradleFlows/
DetectIngestFlowslive inengine/taint.go, the binder marks the curl/wget
output parameters (and synthesises wget's implicit download), and the bash
frontend marks a code-execution sink fed by network content. VCS sync (git clone/fetch/pull) is never an ingest. The CLI envelope tag moves to
flowsh/v3. -
PowerShell abstract variable state (Σ). The PowerShell frontend now
interprets a script through its own bounded abstract variable state Σ
instead of lowering raw parse-tree text, mirroring the bash frontend at the
frontend's own layer (front/ps/state.go,front/ps/wordeval.go, and a
structured-control-flowfront/ps/parse.go). Fabricated literal targets are
gone —$dir/file.txtno longer lands as a confidently-Certainconcrete
path: an effect target is the evaluated value when the word is known and ⊤
otherwise, a URL with a literal host prefix keeps a host-scoped egress
instead of widening to ⊤, and a word's provenance (includingsecret)
flows onto the egress effect so exfiltration pairs on real per-command
dataflow rather than a program-level co-occurrence heuristic. Unset reads
degrade to ⊤ (never the empty string),if/foreach/for/while/do/
tryfork and join Σ per branch, session-state cmdlets (Set-Location,
Set-Variable,Clear-Variable, …) update Σ,@{…}splat literals expand
to their parameter bindings, and lowering is bounded by a deterministic step
counter that unwinds to ⊤. See
ADR-0014.
v0.2.1
Changed
- Report-contract tag bumped to
flowsh/v2across the docs, specs and ADR pointers. A new CI guard asserts that the frozen tag spelled in the live docs matchesanalysis.ToolVersion, so a future bump cannot silently drift; the ADRs keep their historicalflowsh/v1spellings with a pointer to the live contract. - The canonical staging fold is now order-aware, and the canonical form is emitted only when the report actually has effects. Live egress and bottom-targeted effects are kept verbatim, and
commandCallsare de-duplicated so a loop body contributes a single entry.
Fixed
- Hardened the host-shaped grammar feeding the egress gate: reject non-network URL schemes (
file:,data:,mailto:), Windows drive paths and VCS-like dotted local names; accept UNC/SMB authorities, the trailing-dot absolute name, scp paths over IPv6 hosts and the clients' own protocol-prefixed address spellings; bound ports to 1–65535. - A confined numeric-class operand that feeds a
NetEgressdestination now degrades to unresolved egress instead of contributing a directory literal, andsystemctl -H/--hostis tiered as a destination dialect. - Fixed numeric-class confinement so an expansion that can splice a fresh, possibly absolute, path component (an alternate/replacement, a zero-length slice, an out-of-range index or a backslash-escaped
..) degrades to ⊤ rather than claiming a directory. - On the PowerShell path, fall back to the command's intrinsic
ProcSpawnwhen the egress gate drops its only effect, and keep a sub-expression destination as unresolved egress.
See CHANGELOG.md for the full history.
v0.2.0
Added
- Host-shaped egress grammar (Track A — evidence validity).
NetEgress
targets must pass a URL/IPv4/IPv6/scp-styleHostShapedgrammar before
binding, so git subcommands, commit SHAs, pathspecs and awk regexes no longer
fabricate network effects, and secret-tainting can no longer pair with a
phantom egress (C5 consistency). - Canonical effect form and an additive per-command
CommandCallview
(Track D — determinism). A canonical effect set folds staging-file
lifecycles onto the final target, giving downstream consumers a stable,
retry-recognizing signature.
Changed
- Hardened argument-scope binding and bash expansion resolution; extended the
benign corpus. - Fixed a stale description in the README.
v0.1.0
Added
- Initial implementation (
feat(init): initial implementation): the
frontend-agnostic effect-analysis engine (engine/), the bash/POSIX and
PowerShell frontends (front/), the embedded YAML knowledge base
(kb/data/*.yaml), command resolution and flag/operand binding (bind/), the
composition facade (internal/analysis/) and theflowshCLI (cmd/flowsh/). - The conformance corpus under
testdata/corpus/(GuardFall, destructive, benign
and PowerShell cases) with a no-silent-miss regression gate. - A tag-triggered release pipeline (
.github/workflows/release.ymland
.goreleaser.yaml): it validates thevX.Y.Ztag shape, re-runsgo vetand
the test gates on the tagged commit, cross-compiles linux/amd64, linux/arm64,
darwin/arm64 and windows/amd64 with GoReleaser, emits a SHA-256
checksums.txtand publishes a GitHub Release, followed by an SLSA
build-provenance attestation verifiable withgh attestation verify. The
build is reproducible (no injected date or commit hash,mod_timestamppinned
to the commit time) and stamps the tag into the binary through the additive
main.buildVersion, soflowsh --versionreports the release without
affecting the frozen report-contract versions.
Changed
- Hardened the knowledge-base loader to tolerate CRLF/CR input and pinned
repository line endings with a.gitattributes(* text=auto eol=lf). - Enforced lint and dead-code gates (pinned
.golangci.yml, a deadcode gate and
documentation hygiene) and dropped the unreachable exported symbols they flag;
the whole-program binder walkers and the corpus harness moved into test-only
files and the newinternal/corpuspackage, and the CLI's terminal output is
routed through a singlefprint/fprintlnhelper. - Refined prose and punctuation across the contributor guide, README and user
documentation. - Refreshed the README effect-report examples and CLI usage: documented the
--fileflag in the synopsis and options table and updated the annotated
examples (therootprovenance line, the expanded score dimensions, the
resolution/destructivelines and provenexfil:pairs;why,
resolutionanddestructivefields in the JSON sample; therm -rf $HOME
score corrected toCritical).
Fixed
- Closed the silent-miss soundness gaps found in code review across the binder,
both frontends, the engine and the knowledge base. The dominant class violated
the no-silent-miss invariant: a command whose invocation matched no
effect-contributing parameter produced neither an effect nor ⊤, so the report
failed open to a benign result (barereboot,kill -9 -1,xargs rm,
builtineval).- Binder. Matches a leading positional operand by value before falling
back to index order, emits an intrinsicProcSpawnfor a resolved command
that matched nothing, lowerskind:selfparameters unconditionally, keeps a
declared numeric flag as a flag rather than an operand, and derives the
conservative flag from a ⊤ effect. - bash frontend. Confines control-flow signals to subshells, background
jobs and speculative branches; inverts!; resets the status per statement;
runs substitutions inside arithmetic/conditional/letand C-stylefor;
executes array-element and subscript substitutions; unwrapsbuiltin; treats
>&wordas a file redirect; and matchescasepatterns with a
bash-compatible glob matcher. - PowerShell frontend. Resolves aliases in source order and
case-insensitively, shadows a cmdlet by a declared alias or function,
degrades comma lists andswitchto ⊤, and models no-space redirections,
data-file parameters and remote-execution cmdlets. - Engine. Made the effect key injective, kept
Normalize's why-traces in
step with the merged effects, and recognized private-key paths and Windows
volume roots. - Knowledge base. Added an intrinsic
selfeffect for the power-control
commands, the long spellings and siblings of the destructive flags, and
rejected the contradictoryoption+valueFrom:argscombination.
- Binder. Matches a leading positional operand by value before falling
- Made the PowerShell per-parse wall-clock budget advisory under the race
detector (build-tagged constants disable the deadline check under-race
while termination stays guaranteed by the parser's deterministic limits;
ADR-0012). - Widened the CI load-latency budget to 50 ms after measuring the hermetic,
in-memory knowledge-base load on shared runners, superseding ADR-0004's 5 ms
threshold while leaving the embed-and-version decision intact (ADR-0011). - Skipped the sub-tick parse-budget assertions on hosts whose monotonic clock is
too coarse to observe the deadline, and widened the repeated-statement source
so the timeout-to-⊤ path stays covered everywhere (ADR-0013).