Text Classification
Scikit-learn
shell-security
shell-risk
bash
powershell
cmd
coding-agents
tf-idf
logistic-regression
cpu
research
cat-shell-risk-commander
shell-command-classification
shell-command-security
shell-risk-classification
command-risk-detection
agent-security
ai-agent-security
ai-security
tool-call-security
tool-security
posix
windows
linux
powershell-security
tfidf
shell-token-features
binary-classification
local-inference
on-device
cpu-inference
scikit-learn
json-model
a1
Eval Results (legacy)
Instructions to use patronus-studio/cat-shell-risk-commander with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Scikit-learn
How to use patronus-studio/cat-shell-risk-commander with Scikit-learn:
from huggingface_hub import hf_hub_download import joblib model = joblib.load( hf_hub_download("patronus-studio/cat-shell-risk-commander", "sklearn_model.joblib") ) # only load pickle files from sources you trust # read more about it here https://skops.readthedocs.io/en/stable/persistence.html - Notebooks
- Google Colab
- Kaggle
Refresh Cat Shell Risk Commander A1 card and add Kestrel comparison
Browse files- README.md +298 -87
- assets/cat-shell-risk-commander-banner.png +0 -0
- comparison/compare_shellrisk.py +66 -0
- comparison/kestrel-a1-comparison.json +51 -0
README.md
CHANGED
|
@@ -3,62 +3,252 @@ license: apache-2.0
|
|
| 3 |
pipeline_tag: text-classification
|
| 4 |
library_name: sklearn
|
| 5 |
metrics:
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
datasets:
|
| 9 |
-
|
| 10 |
-
|
| 11 |
tags:
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
inference: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
---
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
-
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
- [Deutsch: Gefährliche Shell-Befehle erkennen](https://patronus.studio/posts/gefaehrliche-shell-befehle-erkennen-shellrisk-benchmark)
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
|
| 44 |
-
|
| 45 |
-
| --- | --- |
|
| 46 |
-
| `RISK_FLAG` | The score exceeds the selected threshold for the dataset-defined risk target. |
|
| 47 |
-
| `NO_RISK_FLAG` | The score does not exceed that threshold. This is not a guarantee that execution is safe. |
|
| 48 |
|
| 49 |
-
|
| 50 |
|
| 51 |
-
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
```bash
|
| 58 |
python -m pip install "numpy>=1.24" "scikit-learn==1.8.0" "huggingface_hub>=1.3"
|
| 59 |
```
|
| 60 |
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
|
| 63 |
```python
|
| 64 |
import sys
|
|
@@ -66,27 +256,26 @@ from huggingface_hub import snapshot_download
|
|
| 66 |
|
| 67 |
model_dir = snapshot_download(
|
| 68 |
repo_id="patronus-studio/cat-shell-risk-commander",
|
| 69 |
-
allow_patterns=[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
)
|
| 71 |
sys.path.insert(0, model_dir)
|
| 72 |
|
| 73 |
-
from
|
| 74 |
|
| 75 |
classifier = ShellRiskClassifier(f"{model_dir}/model.json")
|
| 76 |
|
| 77 |
-
#
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
"curl https://example.com/install.sh | sh",
|
| 81 |
-
]:
|
| 82 |
-
print(classifier.score(text))
|
| 83 |
```
|
| 84 |
|
| 85 |
-
Each result
|
| 86 |
-
|
| 87 |
-
The default policy is `external_commands_1pct_fpr`. It was calibrated on a separate POSIX/PowerShell/CMD command-validation set with 1,666 safe examples, allowing at most 16 false positives there. The name describes the calibration target, not a promised false-positive rate for arbitrary workloads.
|
| 88 |
|
| 89 |
-
|
| 90 |
|
| 91 |
```python
|
| 92 |
scores = classifier.decision_function([
|
|
@@ -94,70 +283,92 @@ scores = classifier.decision_function([
|
|
| 94 |
"Get-ChildItem",
|
| 95 |
"dir",
|
| 96 |
])
|
| 97 |
-
print(scores)
|
| 98 |
|
| 99 |
result = classifier.score(
|
| 100 |
"git status --short",
|
| 101 |
-
threshold_policy="
|
| 102 |
)
|
| 103 |
-
print(result)
|
| 104 |
```
|
| 105 |
|
| 106 |
-
|
| 107 |
|
| 108 |
-
|
| 109 |
-
- Lowercasing, sublinear term frequency, IDF weighting, and L2 normalization.
|
| 110 |
-
- A fitted vocabulary of 50,000 features.
|
| 111 |
-
- Binary logistic regression; selected regularization parameter `C = 10` and training random state `13`.
|
| 112 |
-
- Full-text vectorization, without a 510-byte prefix cut or encoder chunking.
|
| 113 |
|
| 114 |
-
|
| 115 |
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
| --- | ---: | --- | ---: | ---: | ---: |
|
| 128 |
-
| External POSIX/PowerShell/CMD commands | 918: 374 risky, 544 safe | `external_commands_1pct_fpr` | 85.83% | 0.92% | 91.71% |
|
| 129 |
-
| Original Bash test | 4,194: 193 risky, 4,001 safe | `original_val_f1` | 85.49% | 0.45% | 87.77% |
|
| 130 |
-
| Complete PowerShell scripts | 2,000: 1,000 per class | `original_val_f1` | 96.10% | 2.80% | 96.63% |
|
| 131 |
|
| 132 |
-
The
|
| 133 |
|
| 134 |
-
The
|
| 135 |
|
| 136 |
-
|
| 137 |
|
| 138 |
-
|
| 139 |
|
| 140 |
-
##
|
| 141 |
|
| 142 |
-
- The
|
| 143 |
-
-
|
| 144 |
-
-
|
| 145 |
-
-
|
| 146 |
-
-
|
| 147 |
-
- Processing the entire text does not guarantee robust detection of hidden or late risks. Runtime and memory use grow with input size; no deployment latency claim is made here.
|
| 148 |
|
| 149 |
-
Use the
|
| 150 |
|
| 151 |
## Repository files
|
| 152 |
|
| 153 |
| File | Contents |
|
| 154 |
-
| --- | --- |
|
| 155 |
-
| `
|
| 156 |
-
| `
|
| 157 |
-
| `
|
| 158 |
-
| `
|
| 159 |
-
| `
|
| 160 |
-
| `
|
| 161 |
-
| `
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
pipeline_tag: text-classification
|
| 4 |
library_name: sklearn
|
| 5 |
metrics:
|
| 6 |
+
- recall
|
| 7 |
+
- precision
|
| 8 |
+
- f1
|
| 9 |
+
- roc_auc
|
| 10 |
+
- average_precision
|
| 11 |
datasets:
|
| 12 |
+
- tomngdev/shell-safety
|
| 13 |
+
- nebius/SWE-agent-trajectories
|
| 14 |
tags:
|
| 15 |
+
- shell-security
|
| 16 |
+
- shell-risk
|
| 17 |
+
- bash
|
| 18 |
+
- powershell
|
| 19 |
+
- cmd
|
| 20 |
+
- coding-agents
|
| 21 |
+
- tf-idf
|
| 22 |
+
- logistic-regression
|
| 23 |
+
- cpu
|
| 24 |
+
- research
|
| 25 |
+
- cat-shell-risk-commander
|
| 26 |
+
- shell-command-classification
|
| 27 |
+
- shell-command-security
|
| 28 |
+
- shell-risk-classification
|
| 29 |
+
- command-risk-detection
|
| 30 |
+
- agent-security
|
| 31 |
+
- ai-agent-security
|
| 32 |
+
- ai-security
|
| 33 |
+
- tool-call-security
|
| 34 |
+
- tool-security
|
| 35 |
+
- posix
|
| 36 |
+
- windows
|
| 37 |
+
- linux
|
| 38 |
+
- powershell-security
|
| 39 |
+
- tfidf
|
| 40 |
+
- shell-token-features
|
| 41 |
+
- binary-classification
|
| 42 |
+
- local-inference
|
| 43 |
+
- on-device
|
| 44 |
+
- cpu-inference
|
| 45 |
+
- scikit-learn
|
| 46 |
+
- json-model
|
| 47 |
+
- a1
|
| 48 |
inference: false
|
| 49 |
+
thumbnail: https://huggingface.co/patronus-studio/cat-shell-risk-commander/resolve/main/assets/cat-shell-risk-commander-banner.png
|
| 50 |
+
model-index:
|
| 51 |
+
- name: Cat Shell Risk Commander A1
|
| 52 |
+
results:
|
| 53 |
+
- task:
|
| 54 |
+
type: text-classification
|
| 55 |
+
name: Shell command risk classification
|
| 56 |
+
dataset:
|
| 57 |
+
name: Cat Shell A1 stored test (5,948 cases)
|
| 58 |
+
type: cat-shell-a1-stored-test
|
| 59 |
+
split: test
|
| 60 |
+
metrics:
|
| 61 |
+
- type: recall
|
| 62 |
+
name: Recall (external_commands_1pct_fpr)
|
| 63 |
+
value: 0.9259
|
| 64 |
+
- type: f1
|
| 65 |
+
name: F1 (external_commands_1pct_fpr; derived from confusion counts)
|
| 66 |
+
value: 0.942029
|
| 67 |
+
- type: roc_auc
|
| 68 |
+
name: AUROC
|
| 69 |
+
value: 0.9966
|
| 70 |
+
- type: false_positive_rate
|
| 71 |
+
name: False-positive rate (external_commands_1pct_fpr)
|
| 72 |
+
value: 0.0086
|
| 73 |
+
- task:
|
| 74 |
+
type: text-classification
|
| 75 |
+
name: Shell command risk classification
|
| 76 |
+
dataset:
|
| 77 |
+
name: Cat Shell A1 stored test (5,948 cases)
|
| 78 |
+
type: cat-shell-a1-stored-test
|
| 79 |
+
split: test
|
| 80 |
+
metrics:
|
| 81 |
+
- type: recall
|
| 82 |
+
name: Recall (block_0_1pct_fpr)
|
| 83 |
+
value: 0.8129
|
| 84 |
+
- type: f1
|
| 85 |
+
name: F1 (block_0_1pct_fpr; derived from confusion counts)
|
| 86 |
+
value: 0.888889
|
| 87 |
+
- type: roc_auc
|
| 88 |
+
name: AUROC
|
| 89 |
+
value: 0.9966
|
| 90 |
+
- type: false_positive_rate
|
| 91 |
+
name: False-positive rate (block_0_1pct_fpr)
|
| 92 |
+
value: 0.0035
|
| 93 |
---
|
| 94 |
|
| 95 |
+

|
| 96 |
|
| 97 |
+
<p align="center"><a href="#quickstart">Quickstart</a> · <a href="#evaluation">Evaluation</a> · <a href="#kestrel-comparison">Kestrel comparison</a> · <a href="#model-and-thresholds">Model & thresholds</a> · <a href="https://patronus.studio/product/ai-models">AI models ↗</a></p>
|
| 98 |
|
| 99 |
+
**Screen shell commands before an agent executes them.**
|
| 100 |
|
| 101 |
+
Cat Shell Risk Commander scores shell commands and scripts locally on CPU. Use its risk flag to route agent tool calls for additional checks, review or policy enforcement. It reads the submitted text without executing it.
|
| 102 |
|
| 103 |
+
| 92.59% recall | 0.86% false positives | 4.09 MB |
|
| 104 |
+
| :---: | :---: | :---: |
|
| 105 |
+
| Across the stored 5,948-case test | At the default threshold policy | JSON model artifact |
|
| 106 |
|
| 107 |
+
**A1 research preview.** The figures above describe the current `format_version: 2` export and its stored evaluation. The model combines character n-grams with shell-token features. It is released under Apache 2.0.
|
| 108 |
|
| 109 |
+
## What the model checks
|
|
|
|
| 110 |
|
| 111 |
+
```text
|
| 112 |
+
Agent tool call · Shell command · Script text
|
| 113 |
+
↓
|
| 114 |
+
Cat Shell Risk Commander
|
| 115 |
+
↓
|
| 116 |
+
Raw score + threshold policy
|
| 117 |
+
↓
|
| 118 |
+
Your execution or review policy
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
| Output | Meaning |
|
| 122 |
+
| :--- | :--- |
|
| 123 |
+
| `RISK_FLAG` | The decision score exceeds the selected risk threshold |
|
| 124 |
+
| `NO_RISK_FLAG` | The decision score does not exceed that threshold |
|
| 125 |
+
|
| 126 |
+
A score describes the submitted text. It does not establish what a command will do in a particular environment. Your application decides whether to block, review or continue; a `NO_RISK_FLAG` is not an execution-safety guarantee.
|
| 127 |
+
|
| 128 |
+
## Model at a glance
|
| 129 |
+
|
| 130 |
+
| Property | Current A1 release |
|
| 131 |
+
| :--- | :--- |
|
| 132 |
+
| Architecture | TF-IDF features with a binary logistic-regression classifier |
|
| 133 |
+
| Character features | 50,000 `char_wb` n-grams, lengths 3 to 5 |
|
| 134 |
+
| Shell-token features | 20,096 features from normalized tokens, token pairs and structural markers |
|
| 135 |
+
| Total features | **70,096**, with one linear coefficient per feature plus an intercept |
|
| 136 |
+
| Artifact | **4,088,897 bytes** (4.09 MB), `model.json`, JSON format version 2 |
|
| 137 |
+
| Input | The complete submitted text; no fixed Transformer token window |
|
| 138 |
+
| Output | Raw decision score, label, flag, threshold and threshold-policy name |
|
| 139 |
+
| Inference | Local CPU, NumPy and scikit-learn; no GPU or neural checkpoint |
|
| 140 |
+
| Default policy | `external_commands_1pct_fpr`, threshold `0.65763` |
|
| 141 |
+
| Runtime loader | `shell_cat_guard_a1.py` and `shell_tokens.py` |
|
| 142 |
+
| License | Apache 2.0 |
|
| 143 |
+
|
| 144 |
+
This artifact is a custom JSON model. Use the included A1 inference module; Transformers `pipeline()` and `AutoModel` do not load it.
|
| 145 |
+
|
| 146 |
+
## Evaluation
|
| 147 |
+
|
| 148 |
+
### Compare operating points on the same test
|
| 149 |
+
|
| 150 |
+
The stored A1 evaluation contains **5,948 cases: 1,053 risky and 4,895 safe**. Both rows below use the same model and test set. Threshold policy names identify calibration settings; their names are not promised false-positive rates for every test or production workload.
|
| 151 |
+
|
| 152 |
+
| Threshold policy | Threshold | Recall ↑ | FPR ↓ | Precision ↑ | F1 ↑ |
|
| 153 |
+
| :--- | ---: | ---: | ---: | ---: | ---: |
|
| 154 |
+
| `external_commands_1pct_fpr` | 0.65763 | 92.59% | 0.86% | 95.87% | 94.20% |
|
| 155 |
+
| `block_0_1pct_fpr` | 2.242267 | 81.29% | 0.35% | 98.05% | 88.89% |
|
| 156 |
+
|
| 157 |
+
The default policy detects **975 of 1,053 risky cases**, with **42 false positives among 4,895 safe cases**. Average precision is **0.9858** and AUROC is **0.9966** on this stored test. Precision and F1 above are calculated from the published confusion counts.
|
| 158 |
+
|
| 159 |
+
[Full A1 evaluation](https://huggingface.co/patronus-studio/cat-shell-risk-commander/blob/main/evaluation.json) · [Export validation](https://huggingface.co/patronus-studio/cat-shell-risk-commander/blob/main/validation.json)
|
| 160 |
+
|
| 161 |
+
### Kestrel comparison
|
| 162 |
+
|
| 163 |
+
**Cross-shell transfer: POSIX, PowerShell and CMD.** The [Patronus ShellRisk experiment](https://patronus.studio/posts/gefaehrliche-shell-befehle-erkennen-shellrisk-benchmark) evaluates 918 held-out commands: 374 risky and 544 safe cases, comprising 735 POSIX, 119 PowerShell and 64 CMD commands.
|
| 164 |
+
|
| 165 |
+
For the calibrated rows, each model's threshold is selected on the **same separate external command-validation set**, allowing at most 16 false positives among 1,666 safe examples. The test cases do not select the thresholds.
|
| 166 |
+
|
| 167 |
+
| Model and operating point | Detected / risky | False positives / safe |
|
| 168 |
+
| :--- | ---: | ---: |
|
| 169 |
+
| **Patronus Linear, same calibration** | **321/374 · 85.8%** | **5/544 · 0.9%** |
|
| 170 |
+
| Patronus Encoder, same calibration | 315/374 · 84.2% | 5/544 · 0.9% |
|
| 171 |
+
| [Kestrel](https://huggingface.co/kontext-security/Kestrel/tree/main), 1% validation FPR | 8/374 · 2.1% | 6/544 · 1.1% |
|
| 172 |
+
| Kestrel, native threshold | 209/374 · 55.9% | 226/544 · 41.5% |
|
| 173 |
+
|
| 174 |
+
At the shared validation-FPR target, the Patronus Linear model detects **85.8%** of risky commands versus **2.1%** for Kestrel, while producing fewer false positives on this external test.
|
| 175 |
+
|
| 176 |
+
**Model versions:** this published experiment covers the previous 50,000-feature Patronus Linear model and a separate Patronus Encoder. It predates the current A1 export; A1's own stored results are reported separately in this card.
|
| 177 |
+
|
| 178 |
+
<details>
|
| 179 |
+
<summary>Comparison methodology and full results</summary>
|
| 180 |
+
|
| 181 |
+
The external experiment uses the official Kestrel weights with the documented Python TF-IDF inference: character-within-word n-grams of lengths 3 to 5, lowercasing, sublinear term frequency, IDF weighting and L2 normalization.
|
| 182 |
+
|
| 183 |
+
Training data differ. Kestrel was released for a different risk-classification task, while the Patronus models receive examples from the expanded sources during fitting. The experiment demonstrates a source-transfer gap and the benefit of adapting to that target distribution, rather than an architecture-only comparison or universal superiority.
|
| 184 |
|
| 185 |
+
[Full published comparison (Deutsch)](https://patronus.studio/posts/gefaehrliche-shell-befehle-erkennen-shellrisk-benchmark) · [English report](https://patronus.studio/en/posts/detecting-risky-shell-commands-shellrisk-benchmark)
|
| 186 |
|
| 187 |
+
The original same-source Bash test answers a different question. [Additional A1/Bash reference data](comparison/kestrel-a1-comparison.json) and the [reproduction script](comparison/compare_shellrisk.py) remain available separately; those values are not the external-transfer results in the table above.
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
</details>
|
| 190 |
|
| 191 |
+
<details>
|
| 192 |
+
<summary>Shell-specific results at the default policy</summary>
|
| 193 |
|
| 194 |
+
These are source-specific subsets, not four independent production evaluations. PowerShell and CMD command-test samples are small; zero observed false positives does not establish a zero underlying false-positive rate.
|
| 195 |
|
| 196 |
+
| Test subset | Cases | Risky cases | Recall ↑ | FPR ↓ |
|
| 197 |
+
| :--- | ---: | ---: | ---: | ---: |
|
| 198 |
+
| Original Bash | 4194 | 193 | 84.97% | 0.25% |
|
| 199 |
+
| External POSIX | 735 | 292 | 90.75% | 2.26% |
|
| 200 |
+
| External PowerShell | 119 | 58 | 98.28% | 0.00% |
|
| 201 |
+
| External CMD | 64 | 24 | 95.83% | 0.00% |
|
| 202 |
+
|
| 203 |
+
</details>
|
| 204 |
+
|
| 205 |
+
<details>
|
| 206 |
+
<summary>All stored test families and false-positive uncertainty</summary>
|
| 207 |
+
|
| 208 |
+
Default policy: `external_commands_1pct_fpr`. FPR upper bounds are copied from the published `fpr_wilson_upper` field. A metric is n/a when the subset has no examples of the required class. Several families have very small sample sizes.
|
| 209 |
+
|
| 210 |
+
| Family | Cases | Risky cases | Recall ↑ | FPR ↓ | Reported Wilson FPR upper bound |
|
| 211 |
+
| :--- | ---: | ---: | ---: | ---: | ---: |
|
| 212 |
+
| `shellsafety_powershell` | 119 | 58 | 98.28% | 0.00% | 5.92% |
|
| 213 |
+
| `shellsafety_posix` | 735 | 292 | 90.75% | 2.26% | 4.10% |
|
| 214 |
+
| `shellsafety_cmd` | 64 | 24 | 95.83% | 0.00% | 8.76% |
|
| 215 |
+
| `original_bash` | 4194 | 193 | 84.97% | 0.25% | 0.46% |
|
| 216 |
+
| `exfil_pipe` | 160 | 160 | 95.63% | n/a | n/a |
|
| 217 |
+
| `credential_store` | 80 | 80 | 97.50% | n/a | n/a |
|
| 218 |
+
| `reverse_shell` | 7 | 7 | 85.71% | n/a | n/a |
|
| 219 |
+
| `remote_download_exec` | 147 | 147 | 98.64% | n/a | n/a |
|
| 220 |
+
| `privilege_escalation` | 8 | 8 | 100.00% | n/a | n/a |
|
| 221 |
+
| `defense_evasion` | 26 | 26 | 96.15% | n/a | n/a |
|
| 222 |
+
| `disk_destruction` | 28 | 28 | 92.86% | n/a | n/a |
|
| 223 |
+
| `resource_dos` | 4 | 4 | 75.00% | n/a | n/a |
|
| 224 |
+
| `payload_decode_exec` | 6 | 6 | 100.00% | n/a | n/a |
|
| 225 |
+
| `agent_log` | 307 | 0 | n/a | 4.56% | 7.51% |
|
| 226 |
+
| `issue_1` | 10 | 10 | 60.00% | n/a | n/a |
|
| 227 |
+
| `issue_2` | 10 | 10 | 100.00% | n/a | n/a |
|
| 228 |
+
| `issue_3` | 2 | 0 | n/a | 0.00% | 65.76% |
|
| 229 |
+
| `pipe_filter` | 10 | 0 | n/a | 10.00% | 40.42% |
|
| 230 |
+
| `near_miss` | 18 | 0 | n/a | 16.67% | 39.22% |
|
| 231 |
+
| `routine_dev` | 13 | 0 | n/a | 30.77% | 57.63% |
|
| 232 |
+
|
| 233 |
+
The stored JSON also contains per-family results for `block_0_1pct_fpr`, confusion counts, ranking metrics and issue-specific checks. This card reports that source without asserting additional split-independence guarantees.
|
| 234 |
+
|
| 235 |
+
</details>
|
| 236 |
+
|
| 237 |
+
### JSON export parity
|
| 238 |
+
|
| 239 |
+
The repository's validation report compares the exported model with the fitted in-memory model on **5,948 examples**. It records **zero changed decisions** at either threshold policy and a maximum absolute score error of approximately **8.53 × 10⁻⁶**. This validates export parity on those examples; it is separate from detection quality.
|
| 240 |
+
|
| 241 |
+
## Quickstart
|
| 242 |
+
|
| 243 |
+
### Install
|
| 244 |
|
| 245 |
```bash
|
| 246 |
python -m pip install "numpy>=1.24" "scikit-learn==1.8.0" "huggingface_hub>=1.3"
|
| 247 |
```
|
| 248 |
|
| 249 |
+
### Score command text locally
|
| 250 |
+
|
| 251 |
+
Download the A1 model together with its matching loader and tokenizer:
|
| 252 |
|
| 253 |
```python
|
| 254 |
import sys
|
|
|
|
| 256 |
|
| 257 |
model_dir = snapshot_download(
|
| 258 |
repo_id="patronus-studio/cat-shell-risk-commander",
|
| 259 |
+
allow_patterns=[
|
| 260 |
+
"model.json",
|
| 261 |
+
"shell_cat_guard_a1.py",
|
| 262 |
+
"shell_tokens.py",
|
| 263 |
+
],
|
| 264 |
)
|
| 265 |
sys.path.insert(0, model_dir)
|
| 266 |
|
| 267 |
+
from shell_cat_guard_a1 import ShellRiskClassifier
|
| 268 |
|
| 269 |
classifier = ShellRiskClassifier(f"{model_dir}/model.json")
|
| 270 |
|
| 271 |
+
# Only the text is classified; these commands are not executed.
|
| 272 |
+
result = classifier.score("git status --short")
|
| 273 |
+
print(result)
|
|
|
|
|
|
|
|
|
|
| 274 |
```
|
| 275 |
|
| 276 |
+
Each result includes `label`, `flagged`, `decision_score`, `threshold` and `threshold_policy`. The decision score is a raw linear logit, not a calibrated probability. A flag is raised when **`decision_score > threshold`**; equality does not raise a flag.
|
|
|
|
|
|
|
| 277 |
|
| 278 |
+
### Batches and threshold policies
|
| 279 |
|
| 280 |
```python
|
| 281 |
scores = classifier.decision_function([
|
|
|
|
| 283 |
"Get-ChildItem",
|
| 284 |
"dir",
|
| 285 |
])
|
|
|
|
| 286 |
|
| 287 |
result = classifier.score(
|
| 288 |
"git status --short",
|
| 289 |
+
threshold_policy="block_0_1pct_fpr",
|
| 290 |
)
|
|
|
|
| 291 |
```
|
| 292 |
|
| 293 |
+
`decision_function` expects a list of strings and returns raw scores. Use `score` for one text and a threshold-based result. Select a policy using representative validation traffic from your application.
|
| 294 |
|
| 295 |
+
## Model and thresholds
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
+
This release has one A1 model with two stored operating points, rather than Transformer precision or ONNX variants.
|
| 298 |
|
| 299 |
+
| Artifact / policy | What to choose it for |
|
| 300 |
+
| :--- | :--- |
|
| 301 |
+
| `model.json` + A1 loader | The current two-block classifier; 70,096 features, about 4.09 MB |
|
| 302 |
+
| `external_commands_1pct_fpr` | Default policy; higher recall in the stored test |
|
| 303 |
+
| `block_0_1pct_fpr` | Stricter score threshold; fewer false positives with lower recall in the stored test |
|
| 304 |
|
| 305 |
+
The legacy `shell_cat_guard.py` expects JSON format version 1. The current `model.json` is format version 2 and requires **`shell_cat_guard_a1.py` plus `shell_tokens.py`**.
|
| 306 |
|
| 307 |
+
## Training data and provenance
|
| 308 |
|
| 309 |
+
The A1 manifest records **23,177 fitting examples**, including **6,253 positive examples**. The remaining 16,924 are negative examples.
|
| 310 |
|
| 311 |
+
| Recorded source | Role in the A1 manifest |
|
| 312 |
+
| :--- | :--- |
|
| 313 |
+
| [Shell Safety](https://huggingface.co/datasets/tomngdev/shell-safety) | Positive and negative commands |
|
| 314 |
+
| Shell Attack Evolution (`zyw-286/shell-attack-evolution-dataset` in the manifest) | Honeypot positives |
|
| 315 |
+
| [Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) | Positive attack commands |
|
| 316 |
+
| [SWE-agent trajectories](https://huggingface.co/datasets/nebius/SWE-agent-trajectories) | Benign agent-command negatives; command strings only |
|
| 317 |
+
| [ShellRisk-Bench](https://github.com/kontext-security/shellrisk-bench) | Original Bash risk target |
|
| 318 |
|
| 319 |
+
The Shell Attack Evolution identifier is retained as recorded provenance; no accessible public Hub dataset was found at that identifier during card preparation.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
+
The fitted model combines 50,000 character-within-word n-gram features with 20,096 shell-token features. The shell-token block includes normalized tokens, token bigrams and markers for command heads, pipe targets and redirection targets. Feature extraction processes text; it does not invoke a shell.
|
| 322 |
|
| 323 |
+
The export identifies its source experiment as `shellrisk-retrain-2026-10-23 (arm A1)`. The earlier article below describes the original ShellRisk experiments and the previous linear model. Its older model sizes, training counts and benchmark values should not be read as A1 results.
|
| 324 |
|
| 325 |
+
[Earlier experiment report (English)](https://patronus.studio/en/posts/detecting-risky-shell-commands-shellrisk-benchmark) · [Deutsch](https://patronus.studio/posts/gefaehrliche-shell-befehle-erkennen-shellrisk-benchmark)
|
| 326 |
|
| 327 |
+
Cat Shell Risk Commander is a Patronus model. It is not an official Kestrel release and does not contain the separate Hadamard encoder discussed in the earlier report.
|
| 328 |
|
| 329 |
+
## Deployment considerations
|
| 330 |
|
| 331 |
+
- The classifier predicts the source-defined risk label of submitted text. Dataset labels are not independently verified execution outcomes.
|
| 332 |
+
- It does not execute commands, inspect your filesystem, resolve variables, decode payloads or simulate a shell. Effects that depend on missing payloads or environment context can be missed.
|
| 333 |
+
- Literal feature patterns are not full shell parsing or document-wide semantic reasoning. Obfuscation, unseen patterns and comment padding can affect classification; the previous model's stress-test results do not establish A1 robustness.
|
| 334 |
+
- Scoring the complete text does not imply a fixed latency or memory footprint. Input size affects cost, and no Cat Shell deployment latency or RAM benchmark is published in this card.
|
| 335 |
+
- Source-specific tests, small subgroups and calibration-policy names do not establish performance on new production traffic. Validate thresholds on your workload.
|
|
|
|
| 336 |
|
| 337 |
+
Use the score alongside execution permissions, sandboxing, explicit policy checks and review where required.
|
| 338 |
|
| 339 |
## Repository files
|
| 340 |
|
| 341 |
| File | Contents |
|
| 342 |
+
| :--- | :--- |
|
| 343 |
+
| `model.json` | Current A1 vocabulary blocks, IDF values, linear coefficients, intercept and threshold policies |
|
| 344 |
+
| `shell_cat_guard_a1.py` | Matching format-version-2 CPU loader and result schema |
|
| 345 |
+
| `shell_tokens.py` | Shell-token feature extraction used by A1 |
|
| 346 |
+
| `shell_cat_guard.py` | Legacy format-version-1 loader |
|
| 347 |
+
| `requirements.txt` | Reference Python dependencies |
|
| 348 |
+
| `evaluation.json` | Current stored A1 results, confusion counts and per-family metrics |
|
| 349 |
+
| `validation.json` | Export parity report |
|
| 350 |
+
| `manifest.json` | Artifact hashes, source experiment and training-source records |
|
| 351 |
+
| `LICENSE` | Apache License 2.0 |
|
| 352 |
+
|
| 353 |
+
The JSON format contains fitted inference parameters and does not require loading a pickle. The reference Python loader reconstructs the TF-IDF vectorizers and applies the linear decision rule.
|
| 354 |
+
|
| 355 |
+
## Citation and license
|
| 356 |
+
|
| 357 |
+
Released under **Apache License 2.0**. Training-source licenses and provenance are recorded in [manifest.json](https://huggingface.co/patronus-studio/cat-shell-risk-commander/blob/main/manifest.json).
|
| 358 |
+
|
| 359 |
+
```bibtex
|
| 360 |
+
@misc{catshellriskcommander2026,
|
| 361 |
+
title = {Cat Shell Risk Commander: Local Shell Risk Classification},
|
| 362 |
+
author = {Patronus Protect},
|
| 363 |
+
year = {2026},
|
| 364 |
+
howpublished = {\url{https://huggingface.co/patronus-studio/cat-shell-risk-commander}}
|
| 365 |
+
}
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
## Patronus Protect
|
| 369 |
+
|
| 370 |
+
**Local protection for prompts, tools and documents.**
|
| 371 |
+
|
| 372 |
+
Explore Patronus Protect, or use our open models in your own application.
|
| 373 |
+
|
| 374 |
+
[Try Patronus Protect for free →](https://patronus.studio) · [Explore our AI models →](https://patronus.studio/product/ai-models) · [Wolf Defender Jev →](https://huggingface.co/patronus-studio/wolf-defender-jev)
|
assets/cat-shell-risk-commander-banner.png
ADDED
|
comparison/compare_shellrisk.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare Cat Shell A1 with published Kestrel verdicts; command text is never executed."""
|
| 2 |
+
import argparse
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
import pyarrow.parquet as pq
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def metrics(labels, predictions):
|
| 12 |
+
tp = int(sum(y and p for y, p in zip(labels, predictions)))
|
| 13 |
+
fp = int(sum(not y and p for y, p in zip(labels, predictions)))
|
| 14 |
+
fn = int(sum(y and not p for y, p in zip(labels, predictions)))
|
| 15 |
+
tn = int(sum(not y and not p for y, p in zip(labels, predictions)))
|
| 16 |
+
return dict(n=len(labels), tp=tp, fp=fp, fn=fn, tn=tn,
|
| 17 |
+
precision=tp/(tp+fp), recall=tp/(tp+fn),
|
| 18 |
+
fpr=fp/(fp+tn), f1=2*tp/(2*tp+fp+fn))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 23 |
+
parser.add_argument('--a1-source', type=Path, required=True)
|
| 24 |
+
parser.add_argument('--gold', type=Path, required=True)
|
| 25 |
+
parser.add_argument('--kestrel-verdicts', type=Path, required=True)
|
| 26 |
+
parser.add_argument('--output', type=Path, required=True)
|
| 27 |
+
args = parser.parse_args()
|
| 28 |
+
sys.path.insert(0, str(args.a1_source.resolve()))
|
| 29 |
+
from shell_cat_guard_a1 import ShellRiskClassifier
|
| 30 |
+
|
| 31 |
+
rows = pq.read_table(args.gold).to_pylist()
|
| 32 |
+
published = {r['id']: r['prediction'] == 'risky' for r in
|
| 33 |
+
map(json.loads, args.kestrel_verdicts.read_text().splitlines())}
|
| 34 |
+
assert len(rows) == len(published) == 4194
|
| 35 |
+
assert set(published) == {r['id'] for r in rows}
|
| 36 |
+
model = ShellRiskClassifier(args.a1_source / 'model.json')
|
| 37 |
+
scores = []
|
| 38 |
+
for start in range(0, len(rows), 128):
|
| 39 |
+
scores.extend(model.decision_function([r['command'] for r in rows[start:start+128]]))
|
| 40 |
+
labels = [r['label'] == 'risky' for r in rows]
|
| 41 |
+
a1 = metrics(labels, [s > model.thresholds[model.default_policy] for s in scores])
|
| 42 |
+
kestrel = metrics(labels, [published[r['id']] for r in rows])
|
| 43 |
+
assert (kestrel['tp'], kestrel['fp']) == (178, 10)
|
| 44 |
+
stored = json.loads((args.a1_source/'evaluation.json').read_text())['original_bash_test'][model.default_policy]
|
| 45 |
+
assert all(a1[k] == stored[k] for k in ['n', 'tp', 'fp', 'fn', 'tn'])
|
| 46 |
+
report = {
|
| 47 |
+
'date': '2026-10-24',
|
| 48 |
+
'dataset': 'kontext-security/ShellRisk-Bench',
|
| 49 |
+
'split': 'test', 'cases': 4194, 'risky': 193, 'safe': 4001,
|
| 50 |
+
'kestrel': {'source': 'Published per-example Kestrel verdicts, native release behaviour', 'metrics': kestrel},
|
| 51 |
+
'cat_shell_a1': {'source': 'Replayed current format-version-2 export with shell_cat_guard_a1.py',
|
| 52 |
+
'policy': model.default_policy,
|
| 53 |
+
'threshold': model.thresholds[model.default_policy], 'metrics': a1},
|
| 54 |
+
'test_data_sha256': hashlib.sha256(args.gold.read_bytes()).hexdigest(),
|
| 55 |
+
'published_kestrel_verdicts_sha256': hashlib.sha256(args.kestrel_verdicts.read_bytes()).hexdigest(),
|
| 56 |
+
'cat_model_sha256': hashlib.sha256((args.a1_source/'model.json').read_bytes()).hexdigest(),
|
| 57 |
+
'notes': ['Both rows use the same fixed 4194-case Bash test. Kestrel values come from published verdicts, not a reimplementation.',
|
| 58 |
+
'Different published/default threshold policies; not a common recalibration or identical-training-data architecture comparison.',
|
| 59 |
+
'Same-source Bash benchmark; not an external-shell transfer or production evaluation.',
|
| 60 |
+
'A1 confusion counts exactly match the current stored evaluation.']}
|
| 61 |
+
args.output.write_text(json.dumps(report, indent=2)+'\n')
|
| 62 |
+
print(json.dumps(report, indent=2))
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == '__main__':
|
| 66 |
+
main()
|
comparison/kestrel-a1-comparison.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"date": "2026-10-24",
|
| 3 |
+
"dataset": "kontext-security/ShellRisk-Bench",
|
| 4 |
+
"split": "test",
|
| 5 |
+
"cases": 4194,
|
| 6 |
+
"risky": 193,
|
| 7 |
+
"safe": 4001,
|
| 8 |
+
"kestrel": {
|
| 9 |
+
"source": "Published per-example Kestrel verdicts, native release behaviour",
|
| 10 |
+
"metrics": {
|
| 11 |
+
"n": 4194,
|
| 12 |
+
"tp": 178,
|
| 13 |
+
"fp": 10,
|
| 14 |
+
"fn": 15,
|
| 15 |
+
"tn": 3991,
|
| 16 |
+
"precision": 0.9468085106382979,
|
| 17 |
+
"recall": 0.9222797927461139,
|
| 18 |
+
"fpr": 0.0024993751562109472,
|
| 19 |
+
"f1": 0.9343832020997376
|
| 20 |
+
},
|
| 21 |
+
"model_revision": "6d6661cac29f8944328f90c8ed3240566cf9f6a1",
|
| 22 |
+
"artifact_sha256": "1df8b3e5f2bfc4e1fe95230ee9b3d37f63aaa461a8551982fcc0dd7103c8221b"
|
| 23 |
+
},
|
| 24 |
+
"cat_shell_a1": {
|
| 25 |
+
"source": "Replayed current format-version-2 export with shell_cat_guard_a1.py",
|
| 26 |
+
"policy": "external_commands_1pct_fpr",
|
| 27 |
+
"threshold": 0.65763,
|
| 28 |
+
"metrics": {
|
| 29 |
+
"n": 4194,
|
| 30 |
+
"tp": 164,
|
| 31 |
+
"fp": 10,
|
| 32 |
+
"fn": 29,
|
| 33 |
+
"tn": 3991,
|
| 34 |
+
"precision": 0.9425287356321839,
|
| 35 |
+
"recall": 0.8497409326424871,
|
| 36 |
+
"fpr": 0.0024993751562109472,
|
| 37 |
+
"f1": 0.8937329700272479
|
| 38 |
+
},
|
| 39 |
+
"model_revision": "2ba751f998dac81b00644fec3b7ca4bf57af08f4"
|
| 40 |
+
},
|
| 41 |
+
"test_data_sha256": "e9df3b50a716ad077eeab6fcc27be57834bc2ff3f28ebeaf48dfd6640e9064c8",
|
| 42 |
+
"published_kestrel_verdicts_sha256": "2d0a3fb080837d1d346da4c422faae4254454af25fb426c3ccba0beb92fb162a",
|
| 43 |
+
"cat_model_sha256": "73f4baaca07a97b5bdef8fd112084a0dfba8fead5ca7f3a0d41660b0ee673556",
|
| 44 |
+
"notes": [
|
| 45 |
+
"Both rows use the same fixed 4194-case Bash test. Kestrel values come from published verdicts, not a reimplementation.",
|
| 46 |
+
"Different published/default threshold policies; not a common recalibration or identical-training-data architecture comparison.",
|
| 47 |
+
"Same-source Bash benchmark; not an external-shell transfer or production evaluation.",
|
| 48 |
+
"A1 confusion counts exactly match the current stored evaluation."
|
| 49 |
+
],
|
| 50 |
+
"dataset_revision": "437467862139b4e9cdd5322024ef3434a67c7ec8"
|
| 51 |
+
}
|