threatcluster-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- threatcluster_cli-0.1.0/.github/workflows/publish.yml +65 -0
- threatcluster_cli-0.1.0/.github/workflows/test.yml +22 -0
- threatcluster_cli-0.1.0/.gitignore +30 -0
- threatcluster_cli-0.1.0/CHANGELOG.md +26 -0
- threatcluster_cli-0.1.0/PKG-INFO +165 -0
- threatcluster_cli-0.1.0/PUBLISHING.md +97 -0
- threatcluster_cli-0.1.0/README.md +145 -0
- threatcluster_cli-0.1.0/pyproject.toml +36 -0
- threatcluster_cli-0.1.0/tc_cli/__init__.py +2 -0
- threatcluster_cli-0.1.0/tc_cli/client.py +225 -0
- threatcluster_cli-0.1.0/tc_cli/commands/__init__.py +0 -0
- threatcluster_cli-0.1.0/tc_cli/commands/auth.py +160 -0
- threatcluster_cli-0.1.0/tc_cli/commands/cluster.py +48 -0
- threatcluster_cli-0.1.0/tc_cli/commands/darkweb.py +67 -0
- threatcluster_cli-0.1.0/tc_cli/commands/entities.py +41 -0
- threatcluster_cli-0.1.0/tc_cli/commands/feeds.py +23 -0
- threatcluster_cli-0.1.0/tc_cli/commands/iocs.py +41 -0
- threatcluster_cli-0.1.0/tc_cli/commands/search.py +61 -0
- threatcluster_cli-0.1.0/tc_cli/commands/threats.py +75 -0
- threatcluster_cli-0.1.0/tc_cli/commands/vulns.py +46 -0
- threatcluster_cli-0.1.0/tc_cli/context.py +16 -0
- threatcluster_cli-0.1.0/tc_cli/credentials.py +102 -0
- threatcluster_cli-0.1.0/tc_cli/main.py +80 -0
- threatcluster_cli-0.1.0/tc_cli/output.py +9 -0
- threatcluster_cli-0.1.0/tc_cli/stdin_helper.py +34 -0
- threatcluster_cli-0.1.0/tc_cli/watch.py +51 -0
- threatcluster_cli-0.1.0/tests/__init__.py +0 -0
- threatcluster_cli-0.1.0/tests/conftest.py +9 -0
- threatcluster_cli-0.1.0/tests/test_client_security.py +66 -0
- threatcluster_cli-0.1.0/tests/test_credentials.py +57 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Publish threatcluster-cli to PyPI
|
|
2
|
+
|
|
3
|
+
# Triggered on a tag push like v0.1.0. The tag must match the version in
|
|
4
|
+
# pyproject.toml; the build step fails fast if they drift.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags:
|
|
8
|
+
- "v*.*.*"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build wheel and sdist
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Install build tooling
|
|
23
|
+
run: python -m pip install --upgrade build
|
|
24
|
+
|
|
25
|
+
- name: Verify tag matches pyproject version
|
|
26
|
+
run: |
|
|
27
|
+
tag="${GITHUB_REF#refs/tags/v}"
|
|
28
|
+
pkg_version=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
29
|
+
if [ "$tag" != "$pkg_version" ]; then
|
|
30
|
+
echo "::error ::tag $tag does not match pyproject version $pkg_version"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: python -m build
|
|
36
|
+
|
|
37
|
+
- name: Upload artifacts
|
|
38
|
+
uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
publish:
|
|
44
|
+
name: Publish to PyPI (Trusted Publishing)
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
# The protected `pypi` environment can require manual approval — see
|
|
48
|
+
# repo Settings -> Environments. Recommended.
|
|
49
|
+
environment:
|
|
50
|
+
name: pypi
|
|
51
|
+
url: https://pypi.org/p/threatcluster-cli
|
|
52
|
+
permissions:
|
|
53
|
+
# OIDC token for PyPI Trusted Publishing — no API token stored anywhere.
|
|
54
|
+
id-token: write
|
|
55
|
+
steps:
|
|
56
|
+
- name: Download build artifacts
|
|
57
|
+
uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
|
|
62
|
+
- name: Publish to PyPI
|
|
63
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
64
|
+
with:
|
|
65
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Test threatcluster-cli
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.10", "3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- name: Install
|
|
20
|
+
run: pip install -e ".[test]"
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: pytest -q
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
|
|
7
|
+
# Testing
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
.tox/
|
|
12
|
+
|
|
13
|
+
# Caches
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.pyc
|
|
16
|
+
*.pyo
|
|
17
|
+
*.pyd
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
|
|
21
|
+
# Environments
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
env/
|
|
25
|
+
|
|
26
|
+
# Editor
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
.DS_Store
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `threatcluster-cli` (the `tc` command) are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
5
|
+
versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — TBD
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Initial release.
|
|
13
|
+
- `tc auth login | status | logout` — paste-token credential flow with
|
|
14
|
+
OS-keyring storage (Keychain / SecretService / Credential Manager) and
|
|
15
|
+
0600-file fallback.
|
|
16
|
+
- Read commands: `tc threats {list,get,iocs,stix}`, `tc iocs {feed,export}`,
|
|
17
|
+
`tc entities {search,get,related,trending}`, `tc vulns {list,get}`,
|
|
18
|
+
`tc darkweb {ransomware victims, breaches, keyword-hits}`,
|
|
19
|
+
`tc feeds {list,get}`.
|
|
20
|
+
- Write command: `tc alerts ack <id>` (only write in v1; idempotent).
|
|
21
|
+
- Bearer-JWT auth model with in-process caching, ~30s pre-expiry refresh,
|
|
22
|
+
one-shot 401 retry.
|
|
23
|
+
- Plaintext-HTTP rejection to non-loopback hosts.
|
|
24
|
+
- `Authorization` and `X-API-Key` redaction from debug logs.
|
|
25
|
+
- Subprocess auth-env hygiene (`TC_PROPAGATE_AUTH=1` to opt in).
|
|
26
|
+
- `--api-key` flag rejected (process-table leak prevention).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: threatcluster-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for ThreatCluster (`tc`)
|
|
5
|
+
Author: ThreatCluster
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
12
|
+
Requires-Dist: keyring<26.0,>=24.0
|
|
13
|
+
Requires-Dist: pydantic<3.0,>=2.6
|
|
14
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
18
|
+
Requires-Dist: respx>=0.21; extra == 'test'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# threatcluster-cli
|
|
22
|
+
|
|
23
|
+
Command-line client for ThreatCluster. Installs the `tc` command.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Once published to PyPI:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pipx install threatcluster-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Until then, install from source:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
git clone <this-repo>
|
|
37
|
+
pipx install ./tc-testing/cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
See `PUBLISHING.md` for the PyPI publish flow.
|
|
41
|
+
|
|
42
|
+
## Authenticate
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
tc auth login
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This runs an Auth0 device-code flow and mints a scoped `tc_agent_*` refresh
|
|
49
|
+
credential, storing it in your OS keyring (Keychain / SecretService /
|
|
50
|
+
Credential Manager). On headless systems it falls back to a 0600 file at
|
|
51
|
+
`~/.config/tc-cli/credentials`.
|
|
52
|
+
|
|
53
|
+
## Use
|
|
54
|
+
|
|
55
|
+
All commands print JSON. Pipe through `jq` for human reading.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
tc threats list --limit 5 | jq '.threats[].title'
|
|
59
|
+
tc entities search "lazarus" --type apt_group
|
|
60
|
+
tc darkweb ransomware victims --days 7
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Cookbook
|
|
64
|
+
|
|
65
|
+
Worked examples for common analyst / agent flows.
|
|
66
|
+
|
|
67
|
+
### Smart search
|
|
68
|
+
|
|
69
|
+
`tc search` merges entity hits and threat clusters into one shape so you don't
|
|
70
|
+
have to know which sub-command applies.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
tc search "Volt Typhoon" --limit 5
|
|
74
|
+
tc search lockbit --only entities
|
|
75
|
+
tc search cisco --only threats --limit 20
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Stdin chaining
|
|
79
|
+
|
|
80
|
+
Any command that takes an `id`/`identifier` accepts `-` to read ids from stdin.
|
|
81
|
+
Compose freely:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
# IOCs for the top 5 trending threats
|
|
85
|
+
tc threats list --limit 5 \
|
|
86
|
+
| jq -r '.threats[].cluster_id' \
|
|
87
|
+
| tc threats iocs - \
|
|
88
|
+
| jq -r '.iocs[]'
|
|
89
|
+
|
|
90
|
+
# STIX export of every threat tagged "ransomware"
|
|
91
|
+
tc threats list --query ransomware --limit 50 \
|
|
92
|
+
| jq -r '.threats[].cluster_id' \
|
|
93
|
+
| tc threats stix -
|
|
94
|
+
|
|
95
|
+
# Open every CVE in your browser (xdg-open / open)
|
|
96
|
+
tc vulns list --severity CRITICAL --limit 5 \
|
|
97
|
+
| jq -r '.cves[].cve_id' \
|
|
98
|
+
| xargs -I{} echo "https://nvd.nist.gov/vuln/detail/{}"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Live feeds (`--watch`)
|
|
102
|
+
|
|
103
|
+
Poll an endpoint and emit only new items as NDJSON. Ctrl+C to stop.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
# New ransomware victims as they're posted
|
|
107
|
+
tc darkweb ransomware victims --watch --interval 60
|
|
108
|
+
|
|
109
|
+
# Pipe live victims into a Slack webhook
|
|
110
|
+
tc darkweb ransomware victims --watch \
|
|
111
|
+
| while read line; do
|
|
112
|
+
echo "$line" | jq -r '"new victim: \(.group): \(.name)"' \
|
|
113
|
+
| curl -X POST -d @- "$SLACK_WEBHOOK_URL"
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
# Watch new threats matching a keyword
|
|
117
|
+
tc threats list --query lockbit --watch --interval 30
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Per-session containment for sub-agents
|
|
121
|
+
|
|
122
|
+
Hand a child process a narrower bearer than your own. The server enforces
|
|
123
|
+
the subset — the child cannot escalate.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
# Read-only sub-agent with a 50-request budget per session
|
|
127
|
+
TC_SCOPES=threats:read \
|
|
128
|
+
TC_SESSION_ID="$(uuidgen)" \
|
|
129
|
+
TC_MAX_REQUESTS=50 \
|
|
130
|
+
tc threats list --limit 5
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Auth diagnostics
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
tc auth status -v # storage backend, bearer jti, key id
|
|
137
|
+
tc auth logout # hard kill: revokes server-side too
|
|
138
|
+
tc auth logout --keep-remote # local-only clear (you're moving the key)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Shell completion
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
tc --install-completion bash # or zsh / fish
|
|
145
|
+
exec $SHELL # restart your shell
|
|
146
|
+
tc thr<TAB> # completes to `tc threats`
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Environment
|
|
150
|
+
|
|
151
|
+
| Var | Purpose |
|
|
152
|
+
|-----------------------|--------------------------------------------------------------|
|
|
153
|
+
| `TC_API_URL` | Override API base (default `https://api.threatcluster.io`). |
|
|
154
|
+
| `TC_REFRESH_TOKEN` | Refresh credential (overrides keyring + file). For CI only. |
|
|
155
|
+
| `TC_SCOPES` | Comma-separated subset of refresh scopes for the bearer. |
|
|
156
|
+
| `TC_PROPAGATE_AUTH` | Set to `1` to propagate auth env to subprocesses. |
|
|
157
|
+
| `TC_DEBUG` | Set to `1` for verbose stderr (auth headers redacted). |
|
|
158
|
+
|
|
159
|
+
## Security notes
|
|
160
|
+
|
|
161
|
+
- Refresh credential is never sent on argv (`--api-key` flag is rejected).
|
|
162
|
+
- Bearer JWTs (15 min ttl) are minted on demand and cached only in memory.
|
|
163
|
+
- The CLI refuses to talk to plaintext `http://` URLs except `127.0.0.1`/`localhost`.
|
|
164
|
+
- Auth env vars are scrubbed from subprocess environments unless
|
|
165
|
+
`TC_PROPAGATE_AUTH=1` is set.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Publishing `threatcluster-cli` to PyPI
|
|
2
|
+
|
|
3
|
+
The PyPI project is `threatcluster-cli`; it installs the `tc` console script.
|
|
4
|
+
|
|
5
|
+
One-time setup (Phases 1–3), then every release is `git tag` + `git push --tags`.
|
|
6
|
+
|
|
7
|
+
## One-time setup
|
|
8
|
+
|
|
9
|
+
### 1. Reserve the name on PyPI
|
|
10
|
+
|
|
11
|
+
1. Sign in / sign up at https://pypi.org.
|
|
12
|
+
2. Enable 2FA (PyPI requires it for publishing).
|
|
13
|
+
3. You don't need to upload a placeholder — Trusted Publishing creates the
|
|
14
|
+
project on first successful publish. You're claiming the name by being the
|
|
15
|
+
first to publish to it.
|
|
16
|
+
|
|
17
|
+
### 2. Configure Trusted Publishing on PyPI
|
|
18
|
+
|
|
19
|
+
PyPI → your account → **Publishing** → **Add a new pending publisher**:
|
|
20
|
+
|
|
21
|
+
| Field | Value |
|
|
22
|
+
|------------------|--------------------------------------------------------------|
|
|
23
|
+
| PyPI project | `threatcluster-cli` |
|
|
24
|
+
| Owner | the GitHub org or user that holds this repo |
|
|
25
|
+
| Repository name | `threatcluster-cli` |
|
|
26
|
+
| Workflow filename| `publish.yml` |
|
|
27
|
+
| Environment name | `pypi` |
|
|
28
|
+
|
|
29
|
+
This binds PyPI to a specific GitHub Actions workflow in a specific repo.
|
|
30
|
+
No API token is created or stored.
|
|
31
|
+
|
|
32
|
+
### 3. Create the GitHub `pypi` environment
|
|
33
|
+
|
|
34
|
+
Repo → **Settings** → **Environments** → **New environment** → name it `pypi`.
|
|
35
|
+
|
|
36
|
+
Highly recommended: enable **Required reviewers** and add yourself. Every
|
|
37
|
+
publish then pauses for a manual click — cheap insurance against an
|
|
38
|
+
accidental tag push.
|
|
39
|
+
|
|
40
|
+
Optionally restrict to tags matching `v*.*.*` under **Deployment branches**.
|
|
41
|
+
|
|
42
|
+
## Every release
|
|
43
|
+
|
|
44
|
+
1. Bump `version` in `cli/pyproject.toml`.
|
|
45
|
+
2. Move the corresponding section in `cli/CHANGELOG.md` from `[Unreleased]`
|
|
46
|
+
to a dated `[X.Y.Z] - YYYY-MM-DD` heading.
|
|
47
|
+
3. Commit and tag:
|
|
48
|
+
```bash
|
|
49
|
+
git commit -am "threatcluster-cli vX.Y.Z"
|
|
50
|
+
git tag vX.Y.Z
|
|
51
|
+
git push && git push --tags
|
|
52
|
+
```
|
|
53
|
+
4. The publish workflow fires. It refuses to publish if the tag and the
|
|
54
|
+
`pyproject.toml` version disagree.
|
|
55
|
+
5. Approve the deploy in the **Actions** tab (if you set up Required
|
|
56
|
+
reviewers).
|
|
57
|
+
6. After the workflow finishes, `pipx install threatcluster-cli==X.Y.Z`
|
|
58
|
+
works for anyone.
|
|
59
|
+
|
|
60
|
+
## What lives where
|
|
61
|
+
|
|
62
|
+
- `cli/pyproject.toml` — package metadata, version, deps, console script (`tc`).
|
|
63
|
+
- `cli/CHANGELOG.md` — release notes (Keep-a-Changelog format).
|
|
64
|
+
- `.github/workflows/publish.yml` — tag-triggered build + publish.
|
|
65
|
+
- `.github/workflows/test.yml` — pytest on push/PR.
|
|
66
|
+
|
|
67
|
+
## Repo layout
|
|
68
|
+
|
|
69
|
+
This repo is the entire `threatcluster-cli` Python package. `python -m build`
|
|
70
|
+
at the repo root produces a self-contained wheel. Tests live in `tests/`.
|
|
71
|
+
|
|
72
|
+
## Yanking a bad release
|
|
73
|
+
|
|
74
|
+
PyPI → project → Manage → Releases → **Yank**. Yank doesn't delete the file
|
|
75
|
+
(downloads still work for pinned versions) but hides it from `pip install`
|
|
76
|
+
without a version pin. Always prefer yank over delete.
|
|
77
|
+
|
|
78
|
+
## TestPyPI dry run (optional, recommended for first release)
|
|
79
|
+
|
|
80
|
+
TestPyPI is a sandbox — same flow, throwaway:
|
|
81
|
+
|
|
82
|
+
1. Sign up at https://test.pypi.org/ (separate account from PyPI).
|
|
83
|
+
2. Add a **pending publisher** there with the same settings as Phase 2.
|
|
84
|
+
3. Temporarily edit the publish step in `publish.yml`:
|
|
85
|
+
```yaml
|
|
86
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
87
|
+
with:
|
|
88
|
+
packages-dir: dist/
|
|
89
|
+
repository-url: https://test.pypi.org/legacy/
|
|
90
|
+
```
|
|
91
|
+
4. Tag a pre-release: `git tag v0.1.0rc1 && git push --tags`.
|
|
92
|
+
5. After it succeeds, install from TestPyPI:
|
|
93
|
+
```bash
|
|
94
|
+
pipx install --pip-args="--index-url https://test.pypi.org/simple/" \
|
|
95
|
+
threatcluster-cli==0.1.0rc1
|
|
96
|
+
```
|
|
97
|
+
6. If happy, revert the workflow change.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# threatcluster-cli
|
|
2
|
+
|
|
3
|
+
Command-line client for ThreatCluster. Installs the `tc` command.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Once published to PyPI:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pipx install threatcluster-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Until then, install from source:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
git clone <this-repo>
|
|
17
|
+
pipx install ./tc-testing/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
See `PUBLISHING.md` for the PyPI publish flow.
|
|
21
|
+
|
|
22
|
+
## Authenticate
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
tc auth login
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This runs an Auth0 device-code flow and mints a scoped `tc_agent_*` refresh
|
|
29
|
+
credential, storing it in your OS keyring (Keychain / SecretService /
|
|
30
|
+
Credential Manager). On headless systems it falls back to a 0600 file at
|
|
31
|
+
`~/.config/tc-cli/credentials`.
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
All commands print JSON. Pipe through `jq` for human reading.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
tc threats list --limit 5 | jq '.threats[].title'
|
|
39
|
+
tc entities search "lazarus" --type apt_group
|
|
40
|
+
tc darkweb ransomware victims --days 7
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Cookbook
|
|
44
|
+
|
|
45
|
+
Worked examples for common analyst / agent flows.
|
|
46
|
+
|
|
47
|
+
### Smart search
|
|
48
|
+
|
|
49
|
+
`tc search` merges entity hits and threat clusters into one shape so you don't
|
|
50
|
+
have to know which sub-command applies.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
tc search "Volt Typhoon" --limit 5
|
|
54
|
+
tc search lockbit --only entities
|
|
55
|
+
tc search cisco --only threats --limit 20
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Stdin chaining
|
|
59
|
+
|
|
60
|
+
Any command that takes an `id`/`identifier` accepts `-` to read ids from stdin.
|
|
61
|
+
Compose freely:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
# IOCs for the top 5 trending threats
|
|
65
|
+
tc threats list --limit 5 \
|
|
66
|
+
| jq -r '.threats[].cluster_id' \
|
|
67
|
+
| tc threats iocs - \
|
|
68
|
+
| jq -r '.iocs[]'
|
|
69
|
+
|
|
70
|
+
# STIX export of every threat tagged "ransomware"
|
|
71
|
+
tc threats list --query ransomware --limit 50 \
|
|
72
|
+
| jq -r '.threats[].cluster_id' \
|
|
73
|
+
| tc threats stix -
|
|
74
|
+
|
|
75
|
+
# Open every CVE in your browser (xdg-open / open)
|
|
76
|
+
tc vulns list --severity CRITICAL --limit 5 \
|
|
77
|
+
| jq -r '.cves[].cve_id' \
|
|
78
|
+
| xargs -I{} echo "https://nvd.nist.gov/vuln/detail/{}"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Live feeds (`--watch`)
|
|
82
|
+
|
|
83
|
+
Poll an endpoint and emit only new items as NDJSON. Ctrl+C to stop.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
# New ransomware victims as they're posted
|
|
87
|
+
tc darkweb ransomware victims --watch --interval 60
|
|
88
|
+
|
|
89
|
+
# Pipe live victims into a Slack webhook
|
|
90
|
+
tc darkweb ransomware victims --watch \
|
|
91
|
+
| while read line; do
|
|
92
|
+
echo "$line" | jq -r '"new victim: \(.group): \(.name)"' \
|
|
93
|
+
| curl -X POST -d @- "$SLACK_WEBHOOK_URL"
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
# Watch new threats matching a keyword
|
|
97
|
+
tc threats list --query lockbit --watch --interval 30
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Per-session containment for sub-agents
|
|
101
|
+
|
|
102
|
+
Hand a child process a narrower bearer than your own. The server enforces
|
|
103
|
+
the subset — the child cannot escalate.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
# Read-only sub-agent with a 50-request budget per session
|
|
107
|
+
TC_SCOPES=threats:read \
|
|
108
|
+
TC_SESSION_ID="$(uuidgen)" \
|
|
109
|
+
TC_MAX_REQUESTS=50 \
|
|
110
|
+
tc threats list --limit 5
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Auth diagnostics
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
tc auth status -v # storage backend, bearer jti, key id
|
|
117
|
+
tc auth logout # hard kill: revokes server-side too
|
|
118
|
+
tc auth logout --keep-remote # local-only clear (you're moving the key)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Shell completion
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
tc --install-completion bash # or zsh / fish
|
|
125
|
+
exec $SHELL # restart your shell
|
|
126
|
+
tc thr<TAB> # completes to `tc threats`
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Environment
|
|
130
|
+
|
|
131
|
+
| Var | Purpose |
|
|
132
|
+
|-----------------------|--------------------------------------------------------------|
|
|
133
|
+
| `TC_API_URL` | Override API base (default `https://api.threatcluster.io`). |
|
|
134
|
+
| `TC_REFRESH_TOKEN` | Refresh credential (overrides keyring + file). For CI only. |
|
|
135
|
+
| `TC_SCOPES` | Comma-separated subset of refresh scopes for the bearer. |
|
|
136
|
+
| `TC_PROPAGATE_AUTH` | Set to `1` to propagate auth env to subprocesses. |
|
|
137
|
+
| `TC_DEBUG` | Set to `1` for verbose stderr (auth headers redacted). |
|
|
138
|
+
|
|
139
|
+
## Security notes
|
|
140
|
+
|
|
141
|
+
- Refresh credential is never sent on argv (`--api-key` flag is rejected).
|
|
142
|
+
- Bearer JWTs (15 min ttl) are minted on demand and cached only in memory.
|
|
143
|
+
- The CLI refuses to talk to plaintext `http://` URLs except `127.0.0.1`/`localhost`.
|
|
144
|
+
- Auth env vars are scrubbed from subprocess environments unless
|
|
145
|
+
`TC_PROPAGATE_AUTH=1` is set.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "threatcluster-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Command-line client for ThreatCluster (`tc`)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "ThreatCluster" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"typer>=0.12,<1.0",
|
|
20
|
+
"httpx>=0.27,<1.0",
|
|
21
|
+
"pydantic>=2.6,<3.0",
|
|
22
|
+
"keyring>=24.0,<26.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
tc = "tc_cli.main:_run"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"pytest-asyncio>=0.23",
|
|
32
|
+
"respx>=0.21",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["tc_cli"]
|