liveapisec 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.
- liveapisec-0.1.0/PKG-INFO +196 -0
- liveapisec-0.1.0/README.md +175 -0
- liveapisec-0.1.0/liveapisec/__init__.py +31 -0
- liveapisec-0.1.0/liveapisec/cli.py +295 -0
- liveapisec-0.1.0/liveapisec/client.py +192 -0
- liveapisec-0.1.0/liveapisec.egg-info/PKG-INFO +196 -0
- liveapisec-0.1.0/liveapisec.egg-info/SOURCES.txt +12 -0
- liveapisec-0.1.0/liveapisec.egg-info/dependency_links.txt +1 -0
- liveapisec-0.1.0/liveapisec.egg-info/entry_points.txt +2 -0
- liveapisec-0.1.0/liveapisec.egg-info/requires.txt +5 -0
- liveapisec-0.1.0/liveapisec.egg-info/top_level.txt +1 -0
- liveapisec-0.1.0/pyproject.toml +45 -0
- liveapisec-0.1.0/setup.cfg +4 -0
- liveapisec-0.1.0/tests/test_cli.py +279 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: liveapisec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LiveAPISec Developer API client — push API specs, run security scans and gate your CI/CD from the command line.
|
|
5
|
+
Author: LiveAPISec
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://liveapisec.com
|
|
8
|
+
Project-URL: Documentation, https://liveapisec.com/settings
|
|
9
|
+
Keywords: security,api,dast,scanning,ci,cd
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: httpx>=0.24
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# liveapisec — CLI/SDK do LiveAPISec Developer API
|
|
23
|
+
|
|
24
|
+
Oficjalny, cienki klient do **LiveAPISec Developer API**. Instalujesz raz,
|
|
25
|
+
używasz w dowolnym projekcie, skrypcie i pipeline CI/CD — bez dashboardu i bez curl.
|
|
26
|
+
|
|
27
|
+
> **Kiedy to jest?** Zamiast ręcznie przechodzić kreatora w panelu, developer
|
|
28
|
+
> pushuje endpointy + opcjonalny token z **swojego** środowiska (CI/CD, agent,
|
|
29
|
+
> skrypt). Token jest generowany u Ciebie i szyfrowany po stronie serwera (AES-256).
|
|
30
|
+
> **Tip: brak tokena = testujemy tylko to, co publiczne.**
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Instalacja
|
|
35
|
+
|
|
36
|
+
Z GitHub (rekomendowane, zanim trafimy na PyPI):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install "liveapisec @ git+https://github.com/LiveApiSec/liveapisec.git"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Po publikacji na PyPI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install liveapisec
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Sprawdź:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
liveapisec --help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Kiedy zainstalujesz raz (np. w obrazie CI, na maszynie dev, w GitHub Actions) —
|
|
55
|
+
komenda `liveapisec` jest dostępna **w każdym projekcie** w tej maszynie.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Konfiguracja
|
|
60
|
+
|
|
61
|
+
Klucz API generujesz raz w panelu: **Settings → Developer API → Create API key**
|
|
62
|
+
(klucz `las_dev_...` pokazywany jest tylko raz — trzymaj go jako secret).
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export LIVEAPISEC_API_KEY=las_dev_... # wymagane
|
|
66
|
+
export LIVEAPISEC_API_URL=https://liveapisec.com # opcjonalne (domyślne)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Można też podać per-komenda: `--api-key` / `--api-url`.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Komendy
|
|
74
|
+
|
|
75
|
+
### 1. `push` — wyślij API (idempotentne, bezpieczne w CI)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
liveapisec push \
|
|
79
|
+
--name my-api \
|
|
80
|
+
--base-url https://api.example.com \
|
|
81
|
+
--endpoint "GET /users" \
|
|
82
|
+
--endpoint "POST /payments"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- Ten sam `name` + `base_url` = **ten sam site** (aktualizacja, nie duplikat) —
|
|
86
|
+
możesz wołać push w każdym buildzie.
|
|
87
|
+
- Zamiast listy endpointów możesz podać OpenAPI: `--openapi-url https://api.example.com/openapi.json`.
|
|
88
|
+
- Opcjonalny token: `--auth-type jwt --auth-token <TOKEN>` (albo `bearer`,
|
|
89
|
+
`cookie --auth-cookie "session=..."`, `api_key --auth-header X-API-Key`).
|
|
90
|
+
|
|
91
|
+
Wynik:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
site 65f...abc: my-api — 2 endpoints, auth=none
|
|
95
|
+
export SITE_ID=65f...abc
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. `scan` — odpal test bezpieczeństwa
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# zwykłe odpalanie (202, nie czeka)
|
|
102
|
+
liveapisec scan --site SITE_ID --branch main --commit "$GITHUB_SHA"
|
|
103
|
+
|
|
104
|
+
# czekaj na wynik i próg błędu dla CI (gate)
|
|
105
|
+
liveapisec scan --site SITE_ID --branch main --commit "$SHA" \
|
|
106
|
+
--wait --fail-on high
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `--wait` — polluje aż skan się zakończy (domyślnie timeout 600 s,
|
|
110
|
+
interwał 3 s; zmiana przez `--timeout` / `--poll-interval`).
|
|
111
|
+
- `--fail-on high` — **exit code 1** gdy znajdzie finding severity `high`/`critical`;
|
|
112
|
+
`--fail-on critical` tylko przy krytycznych; pomiń → zawsze exit 0 (poza błędami).
|
|
113
|
+
|
|
114
|
+
### 3. `status` — stan site'a i ostatnich skanów
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
liveapisec status --site SITE_ID
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. `findings` — wyniki skanu
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
liveapisec findings --site SITE_ID --scan SCAN_ID
|
|
124
|
+
liveapisec findings --site SITE_ID --scan SCAN_ID --json # surowe dane (dla agenta/AI)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 5. `sites` — szczegóły site'a
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
liveapisec sites --site SITE_ID
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## GitHub Actions — pełny przykład (gate na push)
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
name: liveapisec
|
|
139
|
+
on: push
|
|
140
|
+
jobs:
|
|
141
|
+
security-test:
|
|
142
|
+
runs-on: ubuntu-latest
|
|
143
|
+
steps:
|
|
144
|
+
- uses: actions/checkout@v4
|
|
145
|
+
- uses: actions/setup-python@v5
|
|
146
|
+
with: { python-version: "3.12" }
|
|
147
|
+
- name: Install CLI
|
|
148
|
+
run: pip install "liveapisec @ git+https://github.com/LiveApiSec/liveapisec.git"
|
|
149
|
+
- name: Push API + run security test (gate on high)
|
|
150
|
+
env:
|
|
151
|
+
LIVEAPISEC_API_KEY: ${{ secrets.LIVEAPISEC_KEY }}
|
|
152
|
+
run: |
|
|
153
|
+
liveapisec push --name my-api --base-url "$BASE_URL" \
|
|
154
|
+
--endpoint "GET /users" --endpoint "POST /payments"
|
|
155
|
+
liveapisec scan --site "$SITE_ID" \
|
|
156
|
+
--branch "${GITHUB_REF#refs/heads/}" --commit "$GITHUB_SHA" \
|
|
157
|
+
--wait --fail-on high
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> **Dlaczego push jest bezpieczny?** Push jest idempotentny (name+base_url →
|
|
161
|
+
> ten sam site), więc kolejny build nie tworzy śmieci — aktualizuje endpointy
|
|
162
|
+
> i token, a następny `scan` testuje najnowszy stan.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Exit codes
|
|
167
|
+
|
|
168
|
+
| Code | Znaczenie |
|
|
169
|
+
|------|-----------|
|
|
170
|
+
| 0 | OK (brak findings ≥ progu, lub bez `--fail-on`) |
|
|
171
|
+
| 1 | Gate failed — znaleziono findings ≥ `--fail-on` |
|
|
172
|
+
| 2 | Błąd użycia / błąd API / brak klucza |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Rozwój / testy
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install -e ./cli[dev]
|
|
180
|
+
cd cli && python -m pytest tests/ -q
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## API (SDK)
|
|
184
|
+
|
|
185
|
+
Poza CLI pakiet eksportuje też klienta do skryptów:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from liveapisec import LiveAPISec
|
|
189
|
+
|
|
190
|
+
api = LiveAPISec() # LIVEAPISEC_API_KEY z env
|
|
191
|
+
site = api.create_site("my-api", "https://api.example.com",
|
|
192
|
+
endpoints=[{"method": "GET", "path": "/users"}])
|
|
193
|
+
scan = api.trigger_scan(site["site_id"], branch="main", commit="abc")
|
|
194
|
+
done = api.wait_for_scan(site["site_id"], scan["scan_id"])
|
|
195
|
+
blocked = LiveAPISec.findings_above(done["findings"], "high")
|
|
196
|
+
```
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# liveapisec — CLI/SDK do LiveAPISec Developer API
|
|
2
|
+
|
|
3
|
+
Oficjalny, cienki klient do **LiveAPISec Developer API**. Instalujesz raz,
|
|
4
|
+
używasz w dowolnym projekcie, skrypcie i pipeline CI/CD — bez dashboardu i bez curl.
|
|
5
|
+
|
|
6
|
+
> **Kiedy to jest?** Zamiast ręcznie przechodzić kreatora w panelu, developer
|
|
7
|
+
> pushuje endpointy + opcjonalny token z **swojego** środowiska (CI/CD, agent,
|
|
8
|
+
> skrypt). Token jest generowany u Ciebie i szyfrowany po stronie serwera (AES-256).
|
|
9
|
+
> **Tip: brak tokena = testujemy tylko to, co publiczne.**
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Instalacja
|
|
14
|
+
|
|
15
|
+
Z GitHub (rekomendowane, zanim trafimy na PyPI):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "liveapisec @ git+https://github.com/LiveApiSec/liveapisec.git"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Po publikacji na PyPI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install liveapisec
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Sprawdź:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
liveapisec --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Kiedy zainstalujesz raz (np. w obrazie CI, na maszynie dev, w GitHub Actions) —
|
|
34
|
+
komenda `liveapisec` jest dostępna **w każdym projekcie** w tej maszynie.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Konfiguracja
|
|
39
|
+
|
|
40
|
+
Klucz API generujesz raz w panelu: **Settings → Developer API → Create API key**
|
|
41
|
+
(klucz `las_dev_...` pokazywany jest tylko raz — trzymaj go jako secret).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export LIVEAPISEC_API_KEY=las_dev_... # wymagane
|
|
45
|
+
export LIVEAPISEC_API_URL=https://liveapisec.com # opcjonalne (domyślne)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Można też podać per-komenda: `--api-key` / `--api-url`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Komendy
|
|
53
|
+
|
|
54
|
+
### 1. `push` — wyślij API (idempotentne, bezpieczne w CI)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
liveapisec push \
|
|
58
|
+
--name my-api \
|
|
59
|
+
--base-url https://api.example.com \
|
|
60
|
+
--endpoint "GET /users" \
|
|
61
|
+
--endpoint "POST /payments"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Ten sam `name` + `base_url` = **ten sam site** (aktualizacja, nie duplikat) —
|
|
65
|
+
możesz wołać push w każdym buildzie.
|
|
66
|
+
- Zamiast listy endpointów możesz podać OpenAPI: `--openapi-url https://api.example.com/openapi.json`.
|
|
67
|
+
- Opcjonalny token: `--auth-type jwt --auth-token <TOKEN>` (albo `bearer`,
|
|
68
|
+
`cookie --auth-cookie "session=..."`, `api_key --auth-header X-API-Key`).
|
|
69
|
+
|
|
70
|
+
Wynik:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
site 65f...abc: my-api — 2 endpoints, auth=none
|
|
74
|
+
export SITE_ID=65f...abc
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. `scan` — odpal test bezpieczeństwa
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# zwykłe odpalanie (202, nie czeka)
|
|
81
|
+
liveapisec scan --site SITE_ID --branch main --commit "$GITHUB_SHA"
|
|
82
|
+
|
|
83
|
+
# czekaj na wynik i próg błędu dla CI (gate)
|
|
84
|
+
liveapisec scan --site SITE_ID --branch main --commit "$SHA" \
|
|
85
|
+
--wait --fail-on high
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `--wait` — polluje aż skan się zakończy (domyślnie timeout 600 s,
|
|
89
|
+
interwał 3 s; zmiana przez `--timeout` / `--poll-interval`).
|
|
90
|
+
- `--fail-on high` — **exit code 1** gdy znajdzie finding severity `high`/`critical`;
|
|
91
|
+
`--fail-on critical` tylko przy krytycznych; pomiń → zawsze exit 0 (poza błędami).
|
|
92
|
+
|
|
93
|
+
### 3. `status` — stan site'a i ostatnich skanów
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
liveapisec status --site SITE_ID
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 4. `findings` — wyniki skanu
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
liveapisec findings --site SITE_ID --scan SCAN_ID
|
|
103
|
+
liveapisec findings --site SITE_ID --scan SCAN_ID --json # surowe dane (dla agenta/AI)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. `sites` — szczegóły site'a
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
liveapisec sites --site SITE_ID
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## GitHub Actions — pełny przykład (gate na push)
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
name: liveapisec
|
|
118
|
+
on: push
|
|
119
|
+
jobs:
|
|
120
|
+
security-test:
|
|
121
|
+
runs-on: ubuntu-latest
|
|
122
|
+
steps:
|
|
123
|
+
- uses: actions/checkout@v4
|
|
124
|
+
- uses: actions/setup-python@v5
|
|
125
|
+
with: { python-version: "3.12" }
|
|
126
|
+
- name: Install CLI
|
|
127
|
+
run: pip install "liveapisec @ git+https://github.com/LiveApiSec/liveapisec.git"
|
|
128
|
+
- name: Push API + run security test (gate on high)
|
|
129
|
+
env:
|
|
130
|
+
LIVEAPISEC_API_KEY: ${{ secrets.LIVEAPISEC_KEY }}
|
|
131
|
+
run: |
|
|
132
|
+
liveapisec push --name my-api --base-url "$BASE_URL" \
|
|
133
|
+
--endpoint "GET /users" --endpoint "POST /payments"
|
|
134
|
+
liveapisec scan --site "$SITE_ID" \
|
|
135
|
+
--branch "${GITHUB_REF#refs/heads/}" --commit "$GITHUB_SHA" \
|
|
136
|
+
--wait --fail-on high
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> **Dlaczego push jest bezpieczny?** Push jest idempotentny (name+base_url →
|
|
140
|
+
> ten sam site), więc kolejny build nie tworzy śmieci — aktualizuje endpointy
|
|
141
|
+
> i token, a następny `scan` testuje najnowszy stan.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Exit codes
|
|
146
|
+
|
|
147
|
+
| Code | Znaczenie |
|
|
148
|
+
|------|-----------|
|
|
149
|
+
| 0 | OK (brak findings ≥ progu, lub bez `--fail-on`) |
|
|
150
|
+
| 1 | Gate failed — znaleziono findings ≥ `--fail-on` |
|
|
151
|
+
| 2 | Błąd użycia / błąd API / brak klucza |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Rozwój / testy
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pip install -e ./cli[dev]
|
|
159
|
+
cd cli && python -m pytest tests/ -q
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## API (SDK)
|
|
163
|
+
|
|
164
|
+
Poza CLI pakiet eksportuje też klienta do skryptów:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from liveapisec import LiveAPISec
|
|
168
|
+
|
|
169
|
+
api = LiveAPISec() # LIVEAPISEC_API_KEY z env
|
|
170
|
+
site = api.create_site("my-api", "https://api.example.com",
|
|
171
|
+
endpoints=[{"method": "GET", "path": "/users"}])
|
|
172
|
+
scan = api.trigger_scan(site["site_id"], branch="main", commit="abc")
|
|
173
|
+
done = api.wait_for_scan(site["site_id"], scan["scan_id"])
|
|
174
|
+
blocked = LiveAPISec.findings_above(done["findings"], "high")
|
|
175
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""liveapisec — oficjalny klient LiveAPISec Developer API.
|
|
2
|
+
|
|
3
|
+
Instalacja::
|
|
4
|
+
|
|
5
|
+
pip install git+https://github.com/<owner>/<repo>.git#subdirectory=cli
|
|
6
|
+
|
|
7
|
+
Potem w dowolnym projekcie/CI::
|
|
8
|
+
|
|
9
|
+
export LIVEAPISEC_API_KEY=las_dev_...
|
|
10
|
+
liveapisec push --name my-api --base-url https://api.example.com --endpoint "GET /users"
|
|
11
|
+
liveapisec scan --site SITE_ID --wait --fail-on high
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .cli import main
|
|
15
|
+
from .client import (
|
|
16
|
+
DEFAULT_API_URL,
|
|
17
|
+
LiveAPISecError,
|
|
18
|
+
ScanStatus,
|
|
19
|
+
severity_rank,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEFAULT_API_URL",
|
|
26
|
+
"LiveAPISecError",
|
|
27
|
+
"ScanStatus",
|
|
28
|
+
"__version__",
|
|
29
|
+
"main",
|
|
30
|
+
"severity_rank",
|
|
31
|
+
]
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""liveapisec — komendy CLI (TODO 2.25).
|
|
2
|
+
|
|
3
|
+
Komendy:
|
|
4
|
+
push — utwórz/aktualizuj site + endpointy + opcjonalny token (idempotentne)
|
|
5
|
+
scan — odpal skan; --wait czeka na wynik; --fail-on ustawia próg błędu CI
|
|
6
|
+
status — status site'a / ostatnich skanów
|
|
7
|
+
findings — listuj findings (--json)
|
|
8
|
+
sites — pokaż site (endpointy, last_scan)
|
|
9
|
+
|
|
10
|
+
Przykład w CI (gate)::
|
|
11
|
+
|
|
12
|
+
liveapisec push --name my-api --base-url https://api.example.com \\
|
|
13
|
+
--endpoint "GET /users" --endpoint "POST /payments"
|
|
14
|
+
liveapisec scan --site SITE_ID --branch main --commit "$SHA" --wait --fail-on high
|
|
15
|
+
|
|
16
|
+
Exit codes (dla CI):
|
|
17
|
+
0 — ok (brak findings >= progu) 1 — findings >= progu (gate failed)
|
|
18
|
+
2 — błąd użycia / API
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from .client import DEFAULT_API_URL, LiveAPISec, LiveAPISecError
|
|
28
|
+
|
|
29
|
+
_SEV = ["critical", "high", "medium", "low", "info"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_endpoint(value: str) -> dict[str, str]:
|
|
33
|
+
"""'GET /users' → {"method":"GET","path":"/users"}."""
|
|
34
|
+
parts = value.split(None, 1)
|
|
35
|
+
if len(parts) != 2:
|
|
36
|
+
raise argparse.ArgumentTypeError(f"expected 'METHOD /path', got {value!r}")
|
|
37
|
+
method, path = parts
|
|
38
|
+
return {"method": method.upper(), "path": path}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _auth_args(parser: argparse.ArgumentParser) -> None:
|
|
42
|
+
parser.add_argument("--auth-type", choices=["none", "jwt", "bearer", "cookie", "api_key"], default="none")
|
|
43
|
+
parser.add_argument("--auth-token", help="token dla jwt/bearer/api_key")
|
|
44
|
+
parser.add_argument("--auth-cookie", help="pełny nagłówek Cookie dla type=cookie")
|
|
45
|
+
parser.add_argument("--auth-header", default="X-API-Key", help="nazwa nagłówka dla api_key")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_auth(args: argparse.Namespace) -> dict[str, Any] | None:
|
|
49
|
+
if args.auth_type == "none":
|
|
50
|
+
return None
|
|
51
|
+
auth: dict[str, Any] = {"type": args.auth_type}
|
|
52
|
+
if args.auth_type in ("jwt", "bearer", "api_key"):
|
|
53
|
+
auth["token"] = args.auth_token
|
|
54
|
+
if args.auth_type == "cookie":
|
|
55
|
+
auth["cookie"] = args.auth_cookie
|
|
56
|
+
if args.auth_type == "api_key":
|
|
57
|
+
auth["header"] = args.auth_header
|
|
58
|
+
return auth
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _fmt_scan(scan: dict[str, Any]) -> str:
|
|
62
|
+
status = scan.get("status", "?")
|
|
63
|
+
summary = scan.get("summary") or {}
|
|
64
|
+
by_sev = summary.get("by_severity") or {}
|
|
65
|
+
parts = [
|
|
66
|
+
f"scan {scan.get('scan_id')}",
|
|
67
|
+
f"status={status}",
|
|
68
|
+
]
|
|
69
|
+
if scan.get("branch"):
|
|
70
|
+
parts.append(f"branch={scan['branch']}")
|
|
71
|
+
if scan.get("commit"):
|
|
72
|
+
parts.append(f"commit={scan['commit']}")
|
|
73
|
+
if status == "completed":
|
|
74
|
+
sev = " ".join(f"{k}={v}" for k, v in sorted(by_sev.items(), key=lambda kv: _SEV.index(kv[0]) if kv[0] in _SEV else 9))
|
|
75
|
+
parts.append(f"tests={summary.get('tests_run', '?')}")
|
|
76
|
+
parts.append(f"findings={summary.get('findings', 0)}" + (f" ({sev})" if sev else ""))
|
|
77
|
+
return " ".join(parts)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _fmt_finding(f: dict[str, Any]) -> str:
|
|
81
|
+
sev = f.get("severity", "?")
|
|
82
|
+
title = f.get("title") or f.get("category") or "?"
|
|
83
|
+
target = f.get("target") or ""
|
|
84
|
+
line = f"[{sev}] {title}"
|
|
85
|
+
if target:
|
|
86
|
+
line += f" ({target})"
|
|
87
|
+
return line
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _cmd_push(client: LiveAPISec, args: argparse.Namespace) -> int:
|
|
91
|
+
if not args.name:
|
|
92
|
+
print("error: --name is required", file=sys.stderr)
|
|
93
|
+
return 2
|
|
94
|
+
if not args.base_url and not args.site:
|
|
95
|
+
print("error: --base-url is required", file=sys.stderr)
|
|
96
|
+
return 2
|
|
97
|
+
if not args.endpoint and not args.openapi_url:
|
|
98
|
+
print("error: provide at least one --endpoint or --openapi-url", file=sys.stderr)
|
|
99
|
+
return 2
|
|
100
|
+
auth = _build_auth(args)
|
|
101
|
+
if auth and args.auth_type in ("jwt", "bearer", "api_key") and not args.auth_token:
|
|
102
|
+
print(f"error: --auth-token required for auth-type={args.auth_type}", file=sys.stderr)
|
|
103
|
+
return 2
|
|
104
|
+
if auth and args.auth_type == "cookie" and not args.auth_cookie:
|
|
105
|
+
print("error: --auth-cookie required for auth-type=cookie", file=sys.stderr)
|
|
106
|
+
return 2
|
|
107
|
+
|
|
108
|
+
site = client.create_site(
|
|
109
|
+
name=args.name,
|
|
110
|
+
base_url=args.base_url,
|
|
111
|
+
endpoints=args.endpoint,
|
|
112
|
+
openapi_url=args.openapi_url,
|
|
113
|
+
project=args.project,
|
|
114
|
+
auth=auth,
|
|
115
|
+
site_id=args.site,
|
|
116
|
+
)
|
|
117
|
+
if args.json:
|
|
118
|
+
print(LiveAPISec.dump(site))
|
|
119
|
+
else:
|
|
120
|
+
updated = " (updated)" if site.get("updated") else ""
|
|
121
|
+
print(f"site {site['site_id']}{updated}: {site['name']} — {site['endpoints_count']} endpoints, auth={site['auth']}")
|
|
122
|
+
print(f"export SITE_ID={site['site_id']}")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cmd_scan(client: LiveAPISec, args: argparse.Namespace) -> int:
|
|
127
|
+
if not args.site:
|
|
128
|
+
print("error: --site (site_id) is required", file=sys.stderr)
|
|
129
|
+
return 2
|
|
130
|
+
scan = client.trigger_scan(args.site, branch=args.branch, commit=args.commit)
|
|
131
|
+
scan_id = scan["scan_id"]
|
|
132
|
+
if args.json:
|
|
133
|
+
print(LiveAPISec.dump(scan))
|
|
134
|
+
else:
|
|
135
|
+
print(f"scan queued: {scan_id}")
|
|
136
|
+
if not args.wait:
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
if not args.json:
|
|
140
|
+
print("waiting for scan to finish…", file=sys.stderr)
|
|
141
|
+
done = client.wait_for_scan(args.site, scan_id)
|
|
142
|
+
findings = done.get("findings") or []
|
|
143
|
+
if args.json:
|
|
144
|
+
print(LiveAPISec.dump(done))
|
|
145
|
+
else:
|
|
146
|
+
print(_fmt_scan(done))
|
|
147
|
+
|
|
148
|
+
if done.get("status") != "completed":
|
|
149
|
+
return 2 if args.fail_on else 0
|
|
150
|
+
|
|
151
|
+
gate_sev = args.fail_on # "high" | "critical" | ...
|
|
152
|
+
if gate_sev:
|
|
153
|
+
blocked = LiveAPISec.findings_above(findings, gate_sev)
|
|
154
|
+
if blocked:
|
|
155
|
+
if not args.json:
|
|
156
|
+
print(f"\n❌ {len(blocked)} finding(s) at or above {gate_sev} — gate failed:", file=sys.stderr)
|
|
157
|
+
for f in blocked:
|
|
158
|
+
print(" " + _fmt_finding(f), file=sys.stderr)
|
|
159
|
+
return 1
|
|
160
|
+
if not args.json:
|
|
161
|
+
print(f"✅ no findings at or above {gate_sev}")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _cmd_status(client: LiveAPISec, args: argparse.Namespace) -> int:
|
|
166
|
+
if not args.site:
|
|
167
|
+
print("error: --site (site_id) is required", file=sys.stderr)
|
|
168
|
+
return 2
|
|
169
|
+
site = client.get_site(args.site)
|
|
170
|
+
scans = client.list_scans(args.site)
|
|
171
|
+
if args.json:
|
|
172
|
+
print(LiveAPISec.dump({"site": site, "scans": scans[:10]}))
|
|
173
|
+
return 0
|
|
174
|
+
print(f"site {site['site_id']}: {site.get('name')} — {site.get('endpoints_count')} endpoints")
|
|
175
|
+
if site.get("base_url"):
|
|
176
|
+
print(f" base_url: {site['base_url']}")
|
|
177
|
+
if site.get("project"):
|
|
178
|
+
print(f" project: {site['project']}")
|
|
179
|
+
if site.get("last_scan_at"):
|
|
180
|
+
print(f" last_scan_at: {site['last_scan_at']}")
|
|
181
|
+
if not scans:
|
|
182
|
+
print(" (no scans yet)")
|
|
183
|
+
return 0
|
|
184
|
+
print(" recent scans:")
|
|
185
|
+
for s in scans[:5]:
|
|
186
|
+
print(" " + _fmt_scan(s))
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _cmd_findings(client: LiveAPISec, args: argparse.Namespace) -> int:
|
|
191
|
+
if not args.site or not args.scan:
|
|
192
|
+
print("error: --site and --scan are required", file=sys.stderr)
|
|
193
|
+
return 2
|
|
194
|
+
findings = client.get_findings(args.site, args.scan)
|
|
195
|
+
if args.json:
|
|
196
|
+
print(LiveAPISec.dump(findings))
|
|
197
|
+
return 0
|
|
198
|
+
if not findings:
|
|
199
|
+
print("no findings")
|
|
200
|
+
return 0
|
|
201
|
+
for f in findings:
|
|
202
|
+
print(_fmt_finding(f))
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _cmd_sites(client: LiveAPISec, args: argparse.Namespace) -> int:
|
|
207
|
+
if not args.site:
|
|
208
|
+
print("error: --site (site_id) is required", file=sys.stderr)
|
|
209
|
+
return 2
|
|
210
|
+
site = client.get_site(args.site)
|
|
211
|
+
if args.json:
|
|
212
|
+
print(LiveAPISec.dump(site))
|
|
213
|
+
return 0
|
|
214
|
+
print(f"site {site['site_id']}: {site.get('name')} — {site.get('endpoints_count')} endpoints")
|
|
215
|
+
if site.get("base_url"):
|
|
216
|
+
print(f" base_url: {site['base_url']}")
|
|
217
|
+
if site.get("project"):
|
|
218
|
+
print(f" project: {site['project']}")
|
|
219
|
+
print(f" source: {site.get('source')} last_scan_at: {site.get('last_scan_at')}")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
224
|
+
parser = argparse.ArgumentParser(
|
|
225
|
+
prog="liveapisec",
|
|
226
|
+
description="LiveAPISec Developer API — push API specs, run security scans, gate your CI/CD.",
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument("--api-url", help=f"API base URL (default: $LIVEAPISEC_API_URL or {DEFAULT_API_URL})")
|
|
229
|
+
parser.add_argument("--api-key", help="dev API key las_dev_... (default: $LIVEAPISEC_API_KEY)")
|
|
230
|
+
parser.add_argument("--json", action="store_true", help="print raw JSON output")
|
|
231
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
232
|
+
|
|
233
|
+
def _json_flag(p: argparse.ArgumentParser) -> None:
|
|
234
|
+
# --json działa też po nazwie podkomendy (np. `findings ... --json`)
|
|
235
|
+
p.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
|
|
236
|
+
|
|
237
|
+
p_push = sub.add_parser("push", help="create/update a site (idempotent)")
|
|
238
|
+
p_push.add_argument("--name", required=True)
|
|
239
|
+
p_push.add_argument("--base-url")
|
|
240
|
+
p_push.add_argument("--project")
|
|
241
|
+
p_push.add_argument("--endpoint", action="append", type=_parse_endpoint, help="'METHOD /path' (repeatable)")
|
|
242
|
+
p_push.add_argument("--openapi-url", help="URL to OpenAPI spec instead of --endpoint")
|
|
243
|
+
p_push.add_argument("--site", help="existing site_id to update (PUT)")
|
|
244
|
+
_auth_args(p_push)
|
|
245
|
+
_json_flag(p_push)
|
|
246
|
+
p_push.set_defaults(func=_cmd_push)
|
|
247
|
+
|
|
248
|
+
p_scan = sub.add_parser("scan", help="run a security scan (optionally wait + gate)")
|
|
249
|
+
p_scan.add_argument("--site", required=True)
|
|
250
|
+
p_scan.add_argument("--branch")
|
|
251
|
+
p_scan.add_argument("--commit")
|
|
252
|
+
p_scan.add_argument("--wait", action="store_true", help="poll until finished")
|
|
253
|
+
p_scan.add_argument("--fail-on", choices=_SEV, help="exit 1 if findings at/above this severity (default: high)")
|
|
254
|
+
p_scan.add_argument("--poll-interval", type=float, default=3.0)
|
|
255
|
+
p_scan.add_argument("--timeout", type=float, default=600.0)
|
|
256
|
+
_json_flag(p_scan)
|
|
257
|
+
p_scan.set_defaults(func=_cmd_scan)
|
|
258
|
+
|
|
259
|
+
p_status = sub.add_parser("status", help="site status + recent scans")
|
|
260
|
+
p_status.add_argument("--site", required=True)
|
|
261
|
+
_json_flag(p_status)
|
|
262
|
+
p_status.set_defaults(func=_cmd_status)
|
|
263
|
+
|
|
264
|
+
p_find = sub.add_parser("findings", help="list findings for a scan")
|
|
265
|
+
p_find.add_argument("--site", required=True)
|
|
266
|
+
p_find.add_argument("--scan", required=True)
|
|
267
|
+
_json_flag(p_find)
|
|
268
|
+
p_find.set_defaults(func=_cmd_findings)
|
|
269
|
+
|
|
270
|
+
p_sites = sub.add_parser("sites", help="show a site")
|
|
271
|
+
p_sites.add_argument("--site", required=True)
|
|
272
|
+
_json_flag(p_sites)
|
|
273
|
+
p_sites.set_defaults(func=_cmd_sites)
|
|
274
|
+
|
|
275
|
+
return parser
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def main(argv: list[str] | None = None) -> int:
|
|
279
|
+
parser = build_parser()
|
|
280
|
+
args = parser.parse_args(argv)
|
|
281
|
+
args.json = bool(getattr(args, "json", False))
|
|
282
|
+
try:
|
|
283
|
+
client = LiveAPISec(api_url=args.api_url, api_key=args.api_key)
|
|
284
|
+
except LiveAPISecError as exc:
|
|
285
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
286
|
+
return 2
|
|
287
|
+
try:
|
|
288
|
+
return int(args.func(client, args))
|
|
289
|
+
except LiveAPISecError as exc:
|
|
290
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
291
|
+
return 2
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
sys.exit(main())
|