finradar-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 (86) hide show
  1. finradar_cli-0.1.0/.gitignore +16 -0
  2. finradar_cli-0.1.0/CHANGELOG.md +34 -0
  3. finradar_cli-0.1.0/Makefile +15 -0
  4. finradar_cli-0.1.0/PKG-INFO +19 -0
  5. finradar_cli-0.1.0/README.md +239 -0
  6. finradar_cli-0.1.0/RELEASE.md +65 -0
  7. finradar_cli-0.1.0/SECURITY.md +72 -0
  8. finradar_cli-0.1.0/_finradar_main.py +7 -0
  9. finradar_cli-0.1.0/finradar.spec +13 -0
  10. finradar_cli-0.1.0/finradar_cli/__init__.py +1 -0
  11. finradar_cli-0.1.0/finradar_cli/__main__.py +4 -0
  12. finradar_cli-0.1.0/finradar_cli/_generated/__init__.py +96 -0
  13. finradar_cli-0.1.0/finradar_cli/_generated/cmd_13dg.py +23 -0
  14. finradar_cli-0.1.0/finradar_cli/_generated/cmd_13f.py +291 -0
  15. finradar_cli-0.1.0/finradar_cli/_generated/cmd_billing.py +68 -0
  16. finradar_cli-0.1.0/finradar_cli/_generated/cmd_core.py +38 -0
  17. finradar_cli-0.1.0/finradar_cli/_generated/cmd_cusip.py +124 -0
  18. finradar_cli-0.1.0/finradar_cli/_generated/cmd_download.py +23 -0
  19. finradar_cli-0.1.0/finradar_cli/_generated/cmd_facts.py +38 -0
  20. finradar_cli-0.1.0/finradar_cli/_generated/cmd_financials.py +141 -0
  21. finradar_cli-0.1.0/finradar_cli/_generated/cmd_holders.py +53 -0
  22. finradar_cli-0.1.0/finradar_cli/_generated/cmd_index.py +21 -0
  23. finradar_cli-0.1.0/finradar_cli/_generated/cmd_insider.py +322 -0
  24. finradar_cli-0.1.0/finradar_cli/_generated/cmd_market.py +23 -0
  25. finradar_cli-0.1.0/finradar_cli/_generated/cmd_ownership.py +111 -0
  26. finradar_cli-0.1.0/finradar_cli/_generated/cmd_ownership_analytics.py +38 -0
  27. finradar_cli-0.1.0/finradar_cli/_generated/cmd_positions.py +83 -0
  28. finradar_cli-0.1.0/finradar_cli/_generated/cmd_pricing.py +53 -0
  29. finradar_cli-0.1.0/finradar_cli/_generated/cmd_progress.py +23 -0
  30. finradar_cli-0.1.0/finradar_cli/_generated/cmd_search.py +171 -0
  31. finradar_cli-0.1.0/finradar_cli/_generated/cmd_sec.py +141 -0
  32. finradar_cli-0.1.0/finradar_cli/_generated/cmd_status.py +36 -0
  33. finradar_cli-0.1.0/finradar_cli/_generated/cmd_ticker.py +92 -0
  34. finradar_cli-0.1.0/finradar_cli/_generated/cmd_user.py +21 -0
  35. finradar_cli-0.1.0/finradar_cli/_generated/command_index.json +1308 -0
  36. finradar_cli-0.1.0/finradar_cli/_generated/completion/commands.json +373 -0
  37. finradar_cli-0.1.0/finradar_cli/app.py +103 -0
  38. finradar_cli-0.1.0/finradar_cli/commands/__init__.py +0 -0
  39. finradar_cli-0.1.0/finradar_cli/commands/api.py +124 -0
  40. finradar_cli-0.1.0/finradar_cli/commands/auth_cmds.py +64 -0
  41. finradar_cli-0.1.0/finradar_cli/commands/meta.py +83 -0
  42. finradar_cli-0.1.0/finradar_cli/commands/update_cmd.py +234 -0
  43. finradar_cli-0.1.0/finradar_cli/core/__init__.py +0 -0
  44. finradar_cli-0.1.0/finradar_cli/core/auth.py +178 -0
  45. finradar_cli-0.1.0/finradar_cli/core/client.py +95 -0
  46. finradar_cli-0.1.0/finradar_cli/core/coerce.py +59 -0
  47. finradar_cli-0.1.0/finradar_cli/core/config.py +80 -0
  48. finradar_cli-0.1.0/finradar_cli/core/curation.py +17 -0
  49. finradar_cli-0.1.0/finradar_cli/core/errors.py +44 -0
  50. finradar_cli-0.1.0/finradar_cli/core/oauth.py +206 -0
  51. finradar_cli-0.1.0/finradar_cli/core/output.py +49 -0
  52. finradar_cli-0.1.0/finradar_cli/core/spec.py +42 -0
  53. finradar_cli-0.1.0/finradar_cli/data/openapi.json +12797 -0
  54. finradar_cli-0.1.0/finradar_cli/marquee.toml +37 -0
  55. finradar_cli-0.1.0/finradar_cli/type_overrides.toml +6 -0
  56. finradar_cli-0.1.0/generate.py +460 -0
  57. finradar_cli-0.1.0/pyproject.toml +41 -0
  58. finradar_cli-0.1.0/tests/conftest.py +19 -0
  59. finradar_cli-0.1.0/tests/fixtures/mini_openapi.json +24 -0
  60. finradar_cli-0.1.0/tests/test_api_command.py +140 -0
  61. finradar_cli-0.1.0/tests/test_app_smoke.py +29 -0
  62. finradar_cli-0.1.0/tests/test_auth.py +103 -0
  63. finradar_cli-0.1.0/tests/test_auth_cmds.py +39 -0
  64. finradar_cli-0.1.0/tests/test_build_client_oauth.py +52 -0
  65. finradar_cli-0.1.0/tests/test_client.py +136 -0
  66. finradar_cli-0.1.0/tests/test_codegen.py +124 -0
  67. finradar_cli-0.1.0/tests/test_coerce.py +40 -0
  68. finradar_cli-0.1.0/tests/test_command_index.py +67 -0
  69. finradar_cli-0.1.0/tests/test_completion.py +99 -0
  70. finradar_cli-0.1.0/tests/test_completion_cmd.py +11 -0
  71. finradar_cli-0.1.0/tests/test_config.py +46 -0
  72. finradar_cli-0.1.0/tests/test_curation.py +18 -0
  73. finradar_cli-0.1.0/tests/test_errors.py +42 -0
  74. finradar_cli-0.1.0/tests/test_generated_commands_e2e.py +21 -0
  75. finradar_cli-0.1.0/tests/test_login_command.py +36 -0
  76. finradar_cli-0.1.0/tests/test_oauth_discovery.py +109 -0
  77. finradar_cli-0.1.0/tests/test_oauth_pkce.py +15 -0
  78. finradar_cli-0.1.0/tests/test_oauth_request_retry.py +138 -0
  79. finradar_cli-0.1.0/tests/test_oauth_store_refresh.py +58 -0
  80. finradar_cli-0.1.0/tests/test_oauth_token_exchange.py +34 -0
  81. finradar_cli-0.1.0/tests/test_output.py +46 -0
  82. finradar_cli-0.1.0/tests/test_output_encoding.py +49 -0
  83. finradar_cli-0.1.0/tests/test_spec.py +58 -0
  84. finradar_cli-0.1.0/tests/test_update_check.py +27 -0
  85. finradar_cli-0.1.0/tests/test_update_cmd.py +327 -0
  86. finradar_cli-0.1.0/tests/test_version.py +7 -0
@@ -0,0 +1,16 @@
1
+ # PyInstaller build artifacts
2
+ dist/
3
+ build/
4
+ dist_wheel/
5
+ *.whl
6
+
7
+ # Python cache
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+ *.pyd
12
+ .Python
13
+
14
+ # Packaging / egg-info
15
+ *.egg-info/
16
+ *.egg
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to the FinRadar CLI are documented here.
4
+ This project adheres to [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.1.0] — first public release
7
+
8
+ First public release of `finradar`, a command-line client for the FinRadar REST API.
9
+
10
+ ### Added
11
+ - **119 generated noun-verb commands** over the API (`finradar sec filings search`,
12
+ `finradar 13f fund show`, `finradar insider transactions by-ticker`, …) plus a raw
13
+ `finradar api METHOD PATH key=value` escape hatch for any endpoint.
14
+ - **Authentication:** `finradar configure` (API key → OS keychain) and `finradar login`
15
+ (OAuth 2.1 loopback PKCE; requires the backend `cli_access` feature, which ships OFF).
16
+ - **Output formats:** `-o json | table | yaml`; scriptable exit codes (0–7); a
17
+ no-telemetry, channel-aware version nudge.
18
+ - **Distribution:** PyPI wheel (pip/pipx), standalone PyInstaller binaries, a Homebrew
19
+ tap, and a Scoop bucket. `finradar update` self-updates a standalone binary
20
+ (sha256-verified, atomic, frozen-only).
21
+
22
+ ### Security
23
+ - Shipped after an independent pre-release security review (**GO-WITH-FIXES**). The
24
+ one release-gating item (a Windows self-update brick path) and all confirmed
25
+ should-fix findings were remediated before release:
26
+ - Atomic Windows self-update with rollback (no bricked binary on a failed swap).
27
+ - Strict `cli-vN.N.N` tag-format gate (closes a release-pipeline command-injection).
28
+ - SHA-pinned third-party GitHub Actions; least-privilege workflow tokens.
29
+ - Owner-only ACLs on the credential file fallback (Windows + POSIX), created atomically.
30
+ - Transient-safe OAuth token retry (no longer logs the user out on a network blip).
31
+ - OAuth metadata origin + https pinning; idempotent-only HTTP retries; constant-time
32
+ `state` comparison.
33
+ - See `SECURITY.md` for the full posture, the accepted-risk items, and the GA roadmap
34
+ (code-signing/Sigstore, PyPI Trusted Publishing, dependency lock, scope enforcement).
@@ -0,0 +1,15 @@
1
+ SPEC_SRC = ../sandbox_frontend/public/openapi.json
2
+ SPEC_DST = finradar_cli/data/openapi.json
3
+
4
+ .PHONY: sync-spec generate check-drift
5
+
6
+ sync-spec:
7
+ cp $(SPEC_SRC) $(SPEC_DST)
8
+ @python -c "import json;print('synced', json.load(open('$(SPEC_DST)'))['info']['version'])"
9
+
10
+ generate:
11
+ python generate.py
12
+
13
+ check-drift: generate
14
+ git diff --exit-code finradar_cli/_generated finradar_cli/data || \
15
+ (echo 'Generated artifacts out of date — run `make sync-spec` (if the spec moved) then `make generate`, and commit the result' && exit 1)
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: finradar-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for the FinRadar API
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click<9,>=8.2
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: keyring>=25
9
+ Requires-Dist: platformdirs>=4
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: rich>=13
12
+ Requires-Dist: tomli-w>=1.0
13
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
14
+ Requires-Dist: typer>=0.15
15
+ Provides-Extra: dev
16
+ Requires-Dist: mypy>=1.10; extra == 'dev'
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Requires-Dist: respx>=0.21; extra == 'dev'
19
+ Requires-Dist: ruff>=0.5; extra == 'dev'
@@ -0,0 +1,239 @@
1
+ # finradar CLI
2
+
3
+ A typed command-line client for the [FinRadar](https://uat.finradarapi.com) REST API.
4
+ Every public endpoint is available as a `finradar <group> <verb>` command; the raw
5
+ `finradar api` escape hatch reaches anything else.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ **pipx (recommended for Python users)**
12
+ ```bash
13
+ pipx install finradar-cli
14
+ ```
15
+
16
+ **Homebrew (macOS / Linux)**
17
+ ```bash
18
+ brew install MarounAntoun/tap/finradar
19
+ ```
20
+
21
+ **Scoop (Windows)**
22
+ ```powershell
23
+ scoop bucket add finradar https://github.com/MarounAntoun/scoop-finradar
24
+ scoop install finradar
25
+ ```
26
+
27
+ **Standalone binary — no Python required**
28
+
29
+ Download the pre-built binary for your OS from the
30
+ [GitHub Releases page](https://github.com/MarounAntoun/finradar-cli/releases/latest):
31
+
32
+ ```bash
33
+ # macOS / Linux
34
+ curl -sSL https://github.com/MarounAntoun/finradar-cli/releases/latest/download/finradar-linux \
35
+ -o finradar
36
+ chmod +x finradar && sudo mv finradar /usr/local/bin/
37
+ ```
38
+
39
+ Windows: download `finradar-windows.exe` from the Releases page and place it on your `PATH`.
40
+
41
+ **Verify**
42
+ ```bash
43
+ finradar --version
44
+ # finradar 0.1.0 (API spec 3.61.0; base https://uat.finradarapi.com)
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Updates
50
+
51
+ The update method depends on how you installed:
52
+
53
+ | Install method | Update command |
54
+ |---|---|
55
+ | Standalone binary | `finradar update` |
56
+ | pipx | `pipx upgrade finradar-cli` |
57
+ | pip | `pip install -U finradar-cli` |
58
+ | Homebrew | `brew upgrade finradar` |
59
+ | Scoop | `scoop update finradar` |
60
+
61
+ **`finradar update`** — standalone-binary users only. Downloads the latest binary from
62
+ [MarounAntoun/finradar-cli](https://github.com/MarounAntoun/finradar-cli/releases/latest),
63
+ verifies the sha256 sidecar, and atomically replaces the running binary. Exits with code 2
64
+ on any hash mismatch or network error (the existing binary is left untouched).
65
+
66
+ If you run a pip/pipx install and call `finradar update`, the command will tell you to
67
+ use `pipx upgrade finradar-cli` instead — it never touches your Python environment.
68
+
69
+ **Version-skew nudge** — after any successful command, if the live API spec is newer
70
+ than the bundled one the CLI prints a one-line hint to stderr (once per hour; suppress
71
+ with `FINRADAR_NO_UPDATE_CHECK=1`). Standalone-binary users see `finradar update`;
72
+ pip/pipx users see `pipx upgrade finradar-cli`.
73
+
74
+ ---
75
+
76
+ ## Quickstart
77
+
78
+ ### 1. Configure your API key
79
+
80
+ Get your key from the [Credentials page](https://uat.finradarapi.com/account/credentials),
81
+ then run:
82
+
83
+ ```bash
84
+ finradar configure
85
+ # API key: ••••••••••••••••
86
+ # Saved profile 'default' → https://uat.finradarapi.com (key in keychain).
87
+ ```
88
+
89
+ The API key is stored in your **OS keychain** (never in the config file).
90
+ Only the profile's `base_url` and `auth_type` are written to the config file.
91
+ You can also set `FINRADAR_API_KEY` in your environment to override the stored key.
92
+
93
+ ### 2. Search SEC filings
94
+
95
+ ```bash
96
+ finradar sec filings search --ticker AAPL --form-type 10-K --limit 5
97
+ ```
98
+
99
+ ### 3. Call a raw endpoint
100
+
101
+ ```bash
102
+ finradar api GET /api/v1/sec/filings ticker=AAPL formType=10-K limit=5
103
+ ```
104
+
105
+ ### 4. (Optional) Log in with OAuth
106
+
107
+ Browser-based login via OAuth 2.1 PKCE — no API key required:
108
+
109
+ ```bash
110
+ finradar login
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Command model
116
+
117
+ Commands follow a `finradar <group> <verb> [options]` noun-verb pattern. Groups map
118
+ to API tag families; verbs map to HTTP operations.
119
+
120
+ | Group | Example command |
121
+ |---|---|
122
+ | `sec` | `finradar sec filings search --ticker TSLA` |
123
+ | `insiders` | `finradar insiders transactions list --ticker NVDA --transaction-type P` |
124
+ | `holdings` | `finradar holdings list --cik 1067983 --quarter 2024Q4` |
125
+ | `cusip` | `finradar cusip lookup --cusip 037833100` |
126
+
127
+ Run `finradar --help` for the full list of groups, or `finradar <group> --help` for
128
+ operations within a group.
129
+
130
+ ### Check your identity
131
+
132
+ ```bash
133
+ finradar whoami
134
+ # profile: default
135
+ # auth_type: api_key
136
+ # base_url: https://uat.finradarapi.com
137
+ # balance: 198432 (paid)
138
+ ```
139
+
140
+ ### Quick help
141
+
142
+ ```bash
143
+ finradar --help # full group list
144
+ finradar <group> --help # verbs within a group
145
+ finradar <group> <verb> --help # flags for a specific command
146
+ ```
147
+
148
+ ### Raw escape hatch
149
+
150
+ ```bash
151
+ # Any method, any endpoint — key=value pairs become query params (GET/DELETE)
152
+ # or JSON body fields (POST/PUT/PATCH)
153
+ finradar api GET /api/v1/sec/filings ticker=AAPL limit=5
154
+ finradar api POST /api/v1/search query="insider buys"
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Output formats
160
+
161
+ Pass `-o` / `--output` to any command:
162
+
163
+ | Flag | Description |
164
+ |---|---|
165
+ | `-o json` | JSON — compact when piped, pretty on a TTY **(default)** |
166
+ | `-o table` | Pretty table |
167
+ | `-o yaml` | YAML |
168
+
169
+ ```bash
170
+ finradar sec filings search --ticker MSFT -o json | jq '.[0].accessionNo'
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Profiles
176
+
177
+ Store multiple base URL / API key combinations as named profiles:
178
+
179
+ ```bash
180
+ # Create a profile
181
+ finradar configure --profile staging
182
+
183
+ # Use it for one command
184
+ finradar --profile staging sec filings search --ticker AAPL
185
+
186
+ # Or set it for the whole session
187
+ export FINRADAR_PROFILE=staging
188
+ ```
189
+
190
+ Config is stored in your OS config directory:
191
+ `~/.config/finradar/config.toml` (Linux), `~/Library/Application Support/finradar/config.toml` (macOS),
192
+ `%APPDATA%\finradar\config.toml` (Windows).
193
+
194
+ ---
195
+
196
+ ## Shell completion
197
+
198
+ ```bash
199
+ # Bash
200
+ finradar completion install bash
201
+
202
+ # Zsh
203
+ finradar completion install zsh
204
+
205
+ # Fish
206
+ finradar completion install fish
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Exit codes
212
+
213
+ | Code | Meaning |
214
+ |---|---|
215
+ | `0` | OK — request succeeded |
216
+ | `1` | Usage error — bad flags or missing argument |
217
+ | `2` | Network error — DNS failure, timeout, unreachable host |
218
+ | `3` | Auth error — invalid or missing API key / token |
219
+ | `4` | Payment error — insufficient token balance (HTTP 402) |
220
+ | `5` | Rate limit — too many requests (HTTP 429) |
221
+ | `6` | Not found — resource does not exist (HTTP 404) |
222
+ | `7` | Server error — upstream 5xx |
223
+
224
+ ---
225
+
226
+ ## Version skew
227
+
228
+ `finradar --version` prints the CLI version, the bundled API version, and the active
229
+ base URL. If the live API has been updated since the CLI was installed, a version-skew
230
+ notice is printed to stderr after any successful command (once per hour, suppressible
231
+ with `FINRADAR_NO_UPDATE_CHECK=1`).
232
+
233
+ ---
234
+
235
+ ## Source
236
+
237
+ This CLI is generated from the FinRadar OpenAPI spec. Commands are built from the
238
+ same `openapi.json` that powers the `/integrations/openapi` docs — if a new endpoint
239
+ appears in the spec, the next CLI release will expose it as a typed command.
@@ -0,0 +1,65 @@
1
+ # FinRadar CLI — Release Runbook
2
+
3
+ The release is fully automated by `.github/workflows/cli-release.yml`, triggered by
4
+ pushing a `cli-vX.Y.Z` tag. This runbook is the human checklist around it.
5
+
6
+ ## One-time setup (maintainer only — required before the FIRST release)
7
+
8
+ These are account credentials the automation cannot create for itself. Add them as
9
+ **repository → Settings → Secrets and variables → Actions**:
10
+
11
+ 1. **`DIST_PUSH_TOKEN`** — a fine-grained GitHub PAT with **Contents: write** on the
12
+ three public distribution repos: `MarounAntoun/finradar-cli`,
13
+ `MarounAntoun/homebrew-tap`, `MarounAntoun/scoop-finradar`. (The built-in
14
+ `GITHUB_TOKEN` cannot write cross-repo, which is why a PAT is required.)
15
+ 2. **`PYPI_TOKEN`** — a PyPI **project-scoped** API token for `finradar-cli`.
16
+ (GA hardening: replace with OIDC Trusted Publishing — see SECURITY.md.)
17
+
18
+ Recommended repo hardening (supply-chain — see SECURITY.md): branch protection on
19
+ `main` with required reviews; enable Dependabot for `github-actions`.
20
+
21
+ ## Pre-flight (before tagging)
22
+
23
+ - [ ] `main` is green: `cli-ci.yml` passing on the latest commit (all OS × Python legs + drift).
24
+ - [ ] Security review fixes merged (this is in `main` as of the v0.1.0 hardening PR).
25
+ - [ ] Version is intentional. The version lives in **one** place: `cli/finradar_cli/__init__.py`
26
+ (`__version__`). The tag MUST be `cli-v` + that exact value. For the first public
27
+ release that is `cli-v0.1.0`.
28
+ - [ ] `CHANGELOG.md` updated for the version.
29
+ - [ ] The 2 secrets above exist (first release only).
30
+ - [ ] Local sanity: `cd cli && python -m build --wheel && pyinstaller finradar.spec --clean --noconfirm`
31
+ both succeed; `dist/finradar.exe --version` prints the expected version.
32
+
33
+ ## Cut the release
34
+
35
+ ```bash
36
+ git checkout main && git pull
37
+ git tag cli-v0.1.0 # must match __version__ exactly; the workflow gate rejects any other shape
38
+ git push origin cli-v0.1.0
39
+ ```
40
+
41
+ The workflow then, in order:
42
+ 1. **Validates the tag** against `^cli-vN.N.N$` (fails closed on anything else).
43
+ 2. **wheel** → builds + `twine upload --skip-existing` to PyPI; pre-creates the GitHub
44
+ Release on the public `finradar-cli` repo.
45
+ 3. **binaries** (ubuntu/macos/windows) → builds each from the wheel, computes a
46
+ `*.sha256` sidecar, uploads binary + sidecar to the public Release.
47
+ 4. **publish-managers** → patches the canonical Homebrew formula + Scoop manifest with
48
+ the version + per-OS sha256 and pushes them (a guard fails the run if any
49
+ `REPLACE_*` placeholder survives).
50
+
51
+ ## Post-release verification
52
+
53
+ - [ ] `pip install finradar-cli==0.1.0` (fresh venv) → `finradar --version` correct.
54
+ - [ ] GitHub Release on `MarounAntoun/finradar-cli` has 3 binaries + 3 `.sha256` sidecars;
55
+ spot-check one: `sha256sum finradar-linux` matches `finradar-linux.sha256`.
56
+ - [ ] `brew install MarounAntoun/tap/finradar` → `finradar --version` correct.
57
+ - [ ] `scoop bucket add finradar https://github.com/MarounAntoun/scoop-finradar; scoop install finradar`.
58
+ - [ ] From a frozen binary: `finradar update --check` reports "up to date" (it now sees the release).
59
+
60
+ ## Rollback
61
+
62
+ - A bad PyPI release cannot be overwritten — yank it on PyPI and ship `0.1.1`.
63
+ - A bad binary: delete the GitHub Release assets (or the Release) on `finradar-cli`;
64
+ the formula/manifest can be reverted via a follow-up tag. The self-updater is
65
+ fail-closed (it never installs an artifact whose sha256 does not match its sidecar).
@@ -0,0 +1,72 @@
1
+ # FinRadar CLI — Security Policy & v0.1.0 Posture
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Email security reports to **marounantoun97@gmail.com** with `[finradar-cli security]`
6
+ in the subject. Please do not open public issues for vulnerabilities. We aim to
7
+ acknowledge within 72 hours.
8
+
9
+ ## Pre-release security review (v0.1.0)
10
+
11
+ Before the first public release the CLI, its release pipeline, and the additive
12
+ backend `cli_access` token underwent an independent, multi-dimension security review
13
+ (self-updater supply-chain, credential storage, OAuth/PKCE, release CI, backend
14
+ token, dependencies, input handling, transport). Verdict: **GO-WITH-FIXES**.
15
+
16
+ - **0 Critical.** The 3 High findings were downgraded to Medium/Low after adversarial
17
+ verification (none anonymous/external).
18
+ - The one release-gating item — a Windows self-updater path that could leave no
19
+ binary on a failed swap — was fixed (atomic rollback) and mutation-tested.
20
+ - The confirmed should-fix findings were all remediated before release: release-tag
21
+ command-injection gate, SHA-pinned GitHub Actions, owner-only credential-file ACLs
22
+ (Windows + POSIX), transient-safe OAuth token retry, OAuth metadata origin/https
23
+ pinning, idempotent-only HTTP retries, constant-time state comparison.
24
+
25
+ ### Confirmed-correct controls (verified by the review)
26
+
27
+ - **Self-updater:** verify-before-replace (the binary is never swapped in unless its
28
+ sha256 matches the sidecar), fail-closed cleanup of unverified artifacts, TLS
29
+ verification on every fetch, strict 64-hex checksum parsing, frozen-only gating, and
30
+ fail-safe version comparison.
31
+ - **Backend `cli_access` token:** full RS256 verification (signature + exp + aud + iss),
32
+ HS256↔RS256 algorithm-confusion blocked, no JWKS/SSRF (in-process key), structural
33
+ audience separation from MCP, token-class confusion blocked both directions,
34
+ fail-closed when disabled — and it **ships OFF** (`CLI_ACCESS_ENABLED=false`).
35
+ - **Credential storage:** OS keychain preferred; secrets never written to `config.toml`
36
+ or logs; PKCE S256 with a CSPRNG verifier/state; loopback callback binds 127.0.0.1 only.
37
+
38
+ ## Accepted risk for v0.1.0 (tracked for GA)
39
+
40
+ These are real and documented; they are acceptable for an initial `0.1.0` release and
41
+ are scheduled for hardening before a `1.0` GA:
42
+
43
+ 1. **Update integrity == GitHub release integrity.** Binaries are protected by HTTPS +
44
+ a sha256 sidecar fetched from the same release. This defends against transport
45
+ corruption, **not** a compromised release account. Anyone who can publish a release
46
+ (a stolen publishing token, compromised CI, or account takeover) controls both the
47
+ binary and its checksum. **Mitigations in place now:** branch protection, the release
48
+ workflow runs only on a strictly-validated `cli-vN.N.N` tag, secrets are env-only,
49
+ third-party Actions are SHA-pinned. **GA hardening:** sign releases with
50
+ Sigstore/cosign (keyless OIDC) or minisign and verify the *signature* (not just the
51
+ hash) in the updater; add OS code-signing (Authenticode / notarization); require a
52
+ hardware-key-protected, reviewed publish.
53
+ 2. **PyPI publishing uses a long-lived API token**, not OIDC Trusted Publishing.
54
+ *GA:* migrate to `pypa/gh-action-pypi-publish` with `id-token: write`.
55
+ 3. **No hash-pinned dependency lock.** Runtime deps use bounded ranges; the binary /
56
+ brew / scoop channels ship a CI-frozen set. *GA:* add a `--generate-hashes` lock and
57
+ pin build tooling for reproducible builds.
58
+ 4. **`cli_access` is full-access; the `cli` scope is not enforced at the resource
59
+ server.** Intentional for v1, and the feature **ships OFF**. **Hard gate:** before
60
+ `CLI_ACCESS_ENABLED` is ever turned on with anything narrower than a single
61
+ full-access `cli` scope, resource-server scope enforcement MUST land — otherwise a
62
+ future narrower-scope token would still reach write endpoints.
63
+
64
+ ## Supply-chain notes for operators
65
+
66
+ - The official binaries and `*.sha256` sidecars are published only at
67
+ `https://github.com/MarounAntoun/finradar-cli/releases`. The Homebrew formula and
68
+ Scoop manifest reference those release assets.
69
+ - `finradar update` only self-updates a **frozen standalone binary**. pip/pipx
70
+ installs are never modified in place (the updater tells you to `pipx upgrade`).
71
+ - Two repository secrets gate publishing: `DIST_PUSH_TOKEN` (cross-repo `contents:write`)
72
+ and `PYPI_TOKEN`. Both are env-only in CI and never logged.
@@ -0,0 +1,7 @@
1
+ # PyInstaller bootstrap: absolute-import entry point for the one-file binary.
2
+ # The package __main__.py uses relative imports which PyInstaller cannot handle
3
+ # as a top-level script; this thin wrapper uses absolute imports instead.
4
+ from finradar_cli.app import app
5
+
6
+ if __name__ == "__main__":
7
+ app()
@@ -0,0 +1,13 @@
1
+ # PyInstaller one-file `finradar` binary: bundle package data + dynamic keyring backends.
2
+ from PyInstaller.utils.hooks import collect_data_files, collect_submodules
3
+
4
+ datas = collect_data_files("finradar_cli",
5
+ includes=["data/*", "_generated/**/*", "marquee.toml", "type_overrides.toml"])
6
+ hidden = (collect_submodules("keyring.backends")
7
+ + ["finradar_cli._generated"])
8
+
9
+ a = Analysis(["_finradar_main.py"], pathex=["."], datas=datas, hiddenimports=hidden,
10
+ hookspath=[], runtime_hooks=[], excludes=[], noarchive=False)
11
+ pyz = PYZ(a.pure, a.zipped_data)
12
+ exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [],
13
+ name="finradar", debug=False, strip=False, upx=True, console=True)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,96 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+
6
+ from . import cmd_13dg
7
+ from . import cmd_13f
8
+ from . import cmd_billing
9
+ from . import cmd_core
10
+ from . import cmd_cusip
11
+ from . import cmd_download
12
+ from . import cmd_facts
13
+ from . import cmd_financials
14
+ from . import cmd_holders
15
+ from . import cmd_index
16
+ from . import cmd_insider
17
+ from . import cmd_market
18
+ from . import cmd_ownership
19
+ from . import cmd_ownership_analytics
20
+ from . import cmd_positions
21
+ from . import cmd_pricing
22
+ from . import cmd_progress
23
+ from . import cmd_search
24
+ from . import cmd_sec
25
+ from . import cmd_status
26
+ from . import cmd_ticker
27
+ from . import cmd_user
28
+
29
+
30
+ def register(app: typer.Typer) -> None:
31
+ _g_13dg = typer.Typer(no_args_is_help=True)
32
+ app.add_typer(_g_13dg, name='13dg')
33
+ cmd_13dg.build(_g_13dg)
34
+ _g_13f = typer.Typer(no_args_is_help=True)
35
+ app.add_typer(_g_13f, name='13f')
36
+ cmd_13f.build(_g_13f)
37
+ _g_billing = typer.Typer(no_args_is_help=True)
38
+ app.add_typer(_g_billing, name='billing')
39
+ cmd_billing.build(_g_billing)
40
+ _g_core = typer.Typer(no_args_is_help=True)
41
+ app.add_typer(_g_core, name='core')
42
+ cmd_core.build(_g_core)
43
+ _g_cusip = typer.Typer(no_args_is_help=True)
44
+ app.add_typer(_g_cusip, name='cusip')
45
+ cmd_cusip.build(_g_cusip)
46
+ _g_download = typer.Typer(no_args_is_help=True)
47
+ app.add_typer(_g_download, name='download')
48
+ cmd_download.build(_g_download)
49
+ _g_facts = typer.Typer(no_args_is_help=True)
50
+ app.add_typer(_g_facts, name='facts')
51
+ cmd_facts.build(_g_facts)
52
+ _g_financials = typer.Typer(no_args_is_help=True)
53
+ app.add_typer(_g_financials, name='financials')
54
+ cmd_financials.build(_g_financials)
55
+ _g_holders = typer.Typer(no_args_is_help=True)
56
+ app.add_typer(_g_holders, name='holders')
57
+ cmd_holders.build(_g_holders)
58
+ _g_index = typer.Typer(no_args_is_help=True)
59
+ app.add_typer(_g_index, name='index')
60
+ cmd_index.build(_g_index)
61
+ _g_insider = typer.Typer(no_args_is_help=True)
62
+ app.add_typer(_g_insider, name='insider')
63
+ cmd_insider.build(_g_insider)
64
+ _g_market = typer.Typer(no_args_is_help=True)
65
+ app.add_typer(_g_market, name='market')
66
+ cmd_market.build(_g_market)
67
+ _g_ownership = typer.Typer(no_args_is_help=True)
68
+ app.add_typer(_g_ownership, name='ownership')
69
+ cmd_ownership.build(_g_ownership)
70
+ _g_ownership_analytics = typer.Typer(no_args_is_help=True)
71
+ app.add_typer(_g_ownership_analytics, name='ownership-analytics')
72
+ cmd_ownership_analytics.build(_g_ownership_analytics)
73
+ _g_positions = typer.Typer(no_args_is_help=True)
74
+ app.add_typer(_g_positions, name='positions')
75
+ cmd_positions.build(_g_positions)
76
+ _g_pricing = typer.Typer(no_args_is_help=True)
77
+ app.add_typer(_g_pricing, name='pricing')
78
+ cmd_pricing.build(_g_pricing)
79
+ _g_progress = typer.Typer(no_args_is_help=True)
80
+ app.add_typer(_g_progress, name='progress')
81
+ cmd_progress.build(_g_progress)
82
+ _g_search = typer.Typer(no_args_is_help=True)
83
+ app.add_typer(_g_search, name='search')
84
+ cmd_search.build(_g_search)
85
+ _g_sec = typer.Typer(no_args_is_help=True)
86
+ app.add_typer(_g_sec, name='sec')
87
+ cmd_sec.build(_g_sec)
88
+ _g_status = typer.Typer(no_args_is_help=True)
89
+ app.add_typer(_g_status, name='status')
90
+ cmd_status.build(_g_status)
91
+ _g_ticker = typer.Typer(no_args_is_help=True)
92
+ app.add_typer(_g_ticker, name='ticker')
93
+ cmd_ticker.build(_g_ticker)
94
+ _g_user = typer.Typer(no_args_is_help=True)
95
+ app.add_typer(_g_user, name='user')
96
+ cmd_user.build(_g_user)
@@ -0,0 +1,23 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from finradar_cli.commands.api import run_request
6
+
7
+
8
+ def build(parent: typer.Typer) -> None:
9
+ _sub_form_13d = typer.Typer(no_args_is_help=True)
10
+ parent.add_typer(_sub_form_13d, name='form-13d')
11
+
12
+ @_sub_form_13d.command('create', help='Form 13D [cost 0]')
13
+ def cmd_post_api_v1_form_13d(ctx: typer.Context, query_ticker: str = typer.Option(None, '--query.ticker', help="Issuer trading symbol (e.g. 'TWTR'). Case-insensitive."), query_issuerCik: str = typer.Option(None, '--query.issuerCik', help='Issuer CIK number. Leading zeros are stripped automatically.'), query_filerCik: str = typer.Option(None, '--query.filerCik', help='Filer (reporting person) CIK. Leading zeros are stripped automatically.'), query_filerName: str = typer.Option(None, '--query.filerName', help="Partial match on filer name (case-insensitive). E.g. 'Elon Musk'."), query_minOwnership: str = typer.Option(None, '--query.minOwnership', help='Minimum percent of class owned. E.g. 5.0 for >= 5%.'), query_formType: str = typer.Option(None, '--query.formType', help="Filter by form type: 'SC 13D', 'SC 13D/A', 'SC 13G', 'SC 13G/A'. Both SC and SCHEDULE variants accepted."), query_dateRange_from: str = typer.Option(None, '--query.dateRange.from', help="Start date filter (ISO format, e.g. '2025-01-01')."), query_dateRange_to: str = typer.Option(None, '--query.dateRange.to', help="End date filter (ISO format, e.g. '2026-03-14')."), from_: str = typer.Option(None, '--from', help='Pagination offset.'), size: str = typer.Option(None, '--size', help='Page size (max 50).'), sort: str = typer.Option(None, '--sort', help='Sort specification. Supported fields: filedAt, percentOfClass, sharesOwned.'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
14
+ url = '/api/v1/form-13d'
15
+ _pp: dict[str, str] = {}
16
+ for _k, _v in _pp.items():
17
+ url = url.replace("{" + _k + "}", str(_v))
18
+ _kv: list[str] = []
19
+ _body: dict[str, str | None] = {'query.ticker': query_ticker, 'query.issuerCik': query_issuerCik, 'query.filerCik': query_filerCik, 'query.filerName': query_filerName, 'query.minOwnership': query_minOwnership, 'query.formType': query_formType, 'query.dateRange.from': query_dateRange_from, 'query.dateRange.to': query_dateRange_to, 'from': from_, 'size': size, 'sort': sort}
20
+ for _name, _val in _body.items():
21
+ if _val is not None:
22
+ _kv.append(f"{_name}={_val}")
23
+ run_request(ctx.obj, 'POST', url, _kv, data=data)