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.
@@ -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())