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.
Files changed (30) hide show
  1. threatcluster_cli-0.1.0/.github/workflows/publish.yml +65 -0
  2. threatcluster_cli-0.1.0/.github/workflows/test.yml +22 -0
  3. threatcluster_cli-0.1.0/.gitignore +30 -0
  4. threatcluster_cli-0.1.0/CHANGELOG.md +26 -0
  5. threatcluster_cli-0.1.0/PKG-INFO +165 -0
  6. threatcluster_cli-0.1.0/PUBLISHING.md +97 -0
  7. threatcluster_cli-0.1.0/README.md +145 -0
  8. threatcluster_cli-0.1.0/pyproject.toml +36 -0
  9. threatcluster_cli-0.1.0/tc_cli/__init__.py +2 -0
  10. threatcluster_cli-0.1.0/tc_cli/client.py +225 -0
  11. threatcluster_cli-0.1.0/tc_cli/commands/__init__.py +0 -0
  12. threatcluster_cli-0.1.0/tc_cli/commands/auth.py +160 -0
  13. threatcluster_cli-0.1.0/tc_cli/commands/cluster.py +48 -0
  14. threatcluster_cli-0.1.0/tc_cli/commands/darkweb.py +67 -0
  15. threatcluster_cli-0.1.0/tc_cli/commands/entities.py +41 -0
  16. threatcluster_cli-0.1.0/tc_cli/commands/feeds.py +23 -0
  17. threatcluster_cli-0.1.0/tc_cli/commands/iocs.py +41 -0
  18. threatcluster_cli-0.1.0/tc_cli/commands/search.py +61 -0
  19. threatcluster_cli-0.1.0/tc_cli/commands/threats.py +75 -0
  20. threatcluster_cli-0.1.0/tc_cli/commands/vulns.py +46 -0
  21. threatcluster_cli-0.1.0/tc_cli/context.py +16 -0
  22. threatcluster_cli-0.1.0/tc_cli/credentials.py +102 -0
  23. threatcluster_cli-0.1.0/tc_cli/main.py +80 -0
  24. threatcluster_cli-0.1.0/tc_cli/output.py +9 -0
  25. threatcluster_cli-0.1.0/tc_cli/stdin_helper.py +34 -0
  26. threatcluster_cli-0.1.0/tc_cli/watch.py +51 -0
  27. threatcluster_cli-0.1.0/tests/__init__.py +0 -0
  28. threatcluster_cli-0.1.0/tests/conftest.py +9 -0
  29. threatcluster_cli-0.1.0/tests/test_client_security.py +66 -0
  30. 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"]
@@ -0,0 +1,2 @@
1
+ """ThreatCluster CLI."""
2
+ __version__ = "0.1.0"