liveapisec 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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
+ ]
liveapisec/cli.py ADDED
@@ -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())
liveapisec/client.py ADDED
@@ -0,0 +1,192 @@
1
+ """Cienki klient HTTP dla LiveAPISec Developer API (TODO 2.25).
2
+
3
+ Wrapsuje endpointy /developers/* tak, żeby dało się ich używać z konsoli,
4
+ CI/CD i skryptów — bez curl i bez dashboardu.
5
+
6
+ Auth: `Authorization: Bearer <LIVEAPISEC_API_KEY>` (klucz `las_dev_...`
7
+ wygenerowany w Settings → Developer API). Token dewelopera (JWT/cookie/API-key)
8
+ jest wysyłany w payloadzie i szyfrowany po stronie serwera (AES-256).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import time
16
+ from typing import Any
17
+
18
+ import httpx
19
+
20
+ DEFAULT_API_URL = "https://liveapisec.com"
21
+ ENV_API_URL = "LIVEAPISEC_API_URL"
22
+ ENV_API_KEY = "LIVEAPISEC_API_KEY"
23
+
24
+ # Kolejność istotności severity (do gate'ów w CI).
25
+ _SEV_ORDER = ["critical", "high", "medium", "low", "info"]
26
+
27
+
28
+ def severity_rank(severity: str) -> int:
29
+ """0 = critical (najgorzej) … 4 = info. Nieznane → 5 (poniżej info)."""
30
+ try:
31
+ return _SEV_ORDER.index(severity)
32
+ except ValueError:
33
+ return len(_SEV_ORDER)
34
+
35
+
36
+ class LiveAPISecError(RuntimeError):
37
+ """Błąd API: status HTTP + title/detail (RFC 7807)."""
38
+
39
+ def __init__(self, status: int | None, title: str, detail: str = "") -> None:
40
+ super().__init__(f"{title}: {detail}".strip(" :"))
41
+ self.status = status
42
+ self.title = title
43
+ self.detail = detail
44
+
45
+
46
+ class ScanStatus:
47
+ """Statusy skanu (jak w UI)."""
48
+
49
+ QUEUED = "queued"
50
+ RUNNING = "running"
51
+ COMPLETED = "completed"
52
+ FAILED = "failed"
53
+
54
+
55
+ def api_url_from_env() -> str:
56
+ return os.environ.get(ENV_API_URL, DEFAULT_API_URL).rstrip("/")
57
+
58
+
59
+ class LiveAPISec:
60
+ """Klient Developer API. `api_url`/`api_key` z env (LIVEAPISEC_API_URL/KEY)."""
61
+
62
+ def __init__(
63
+ self,
64
+ api_url: str | None = None,
65
+ api_key: str | None = None,
66
+ timeout: float = 30.0,
67
+ transport: httpx.BaseTransport | None = None,
68
+ ) -> None:
69
+ self.api_url = (api_url or api_url_from_env()).rstrip("/")
70
+ self.api_key = api_key or os.environ.get(ENV_API_KEY, "")
71
+ if not self.api_key:
72
+ raise LiveAPISecError(
73
+ None,
74
+ "Missing API key",
75
+ f"set {ENV_API_KEY}=las_dev_... (Settings → Developer API) or pass --api-key",
76
+ )
77
+ self.timeout = timeout
78
+ self._transport = transport
79
+
80
+ # -- transport -----------------------------------------------------------
81
+ def _headers(self) -> dict[str, str]:
82
+ return {
83
+ "Authorization": f"Bearer {self.api_key}",
84
+ "Content-Type": "application/json",
85
+ }
86
+
87
+ def _request(self, method: str, path: str, **kw: Any) -> Any:
88
+ url = f"{self.api_url}{path}"
89
+ try:
90
+ with httpx.Client(transport=self._transport) as client:
91
+ resp = client.request(method, url, headers=self._headers(), timeout=self.timeout, **kw)
92
+ except httpx.HTTPError as exc:
93
+ raise LiveAPISecError(None, "Connection error", str(exc)) from exc
94
+ if resp.status_code >= 400:
95
+ try:
96
+ body = resp.json()
97
+ title = body.get("title", "Error")
98
+ detail = body.get("detail", resp.text[:300])
99
+ except Exception: # noqa: BLE001
100
+ title, detail = "Error", resp.text[:300]
101
+ raise LiveAPISecError(resp.status_code, title, detail)
102
+ if resp.status_code == 204 or not resp.content:
103
+ return None
104
+ return resp.json()
105
+
106
+ # -- keys / sites ---------------------------------------------------------
107
+ def create_site(
108
+ self,
109
+ name: str,
110
+ base_url: str,
111
+ endpoints: list[dict[str, str]] | None = None,
112
+ openapi_url: str | None = None,
113
+ project: str | None = None,
114
+ auth: dict[str, Any] | None = None,
115
+ site_id: str | None = None,
116
+ ) -> dict[str, Any]:
117
+ """Push site (idempotentny wg nazwy+base_url). Bez `site_id` → POST (create/update),
118
+ z `site_id` → PUT (explicit update)."""
119
+ payload: dict[str, Any] = {"name": name, "base_url": base_url}
120
+ if endpoints:
121
+ payload["endpoints"] = endpoints
122
+ if openapi_url:
123
+ payload["openapi_url"] = openapi_url
124
+ if project:
125
+ payload["project"] = project
126
+ if auth:
127
+ payload["auth"] = auth
128
+ if site_id:
129
+ return self._request("PUT", f"/developers/sites/{site_id}", json=payload)
130
+ return self._request("POST", "/developers/sites", json=payload)
131
+
132
+ def get_site(self, site_id: str) -> dict[str, Any]:
133
+ return self._request("GET", f"/developers/sites/{site_id}")
134
+
135
+ # -- scans ----------------------------------------------------------------
136
+ def trigger_scan(
137
+ self, site_id: str, branch: str | None = None, commit: str | None = None
138
+ ) -> dict[str, Any]:
139
+ """Odpal skan (202). Zwraca {scan_id, status, branch, commit}."""
140
+ payload: dict[str, Any] = {}
141
+ if branch:
142
+ payload["branch"] = branch
143
+ if commit:
144
+ payload["commit"] = commit
145
+ return self._request("POST", f"/developers/sites/{site_id}/scans", json=payload)
146
+
147
+ def list_scans(self, site_id: str) -> list[dict[str, Any]]:
148
+ return self._request("GET", f"/developers/sites/{site_id}/scans")
149
+
150
+ def get_scan(self, site_id: str, scan_id: str) -> dict[str, Any] | None:
151
+ """Pojedynczy skan (przez listę — brak dedykowanego GET scan)."""
152
+ for s in self.list_scans(site_id):
153
+ if s.get("scan_id") == scan_id:
154
+ return s
155
+ return None
156
+
157
+ def get_findings(self, site_id: str, scan_id: str) -> list[dict[str, Any]]:
158
+ return self._request("GET", f"/developers/sites/{site_id}/scans/{scan_id}/findings")
159
+
160
+ # -- helpers dla CI --------------------------------------------------------
161
+ def wait_for_scan(
162
+ self,
163
+ site_id: str,
164
+ scan_id: str,
165
+ poll_interval: float = 3.0,
166
+ timeout: float = 600.0,
167
+ ) -> dict[str, Any]:
168
+ """Polluj aż skan się zakończy (completed/failed). Zwraca skan + findings."""
169
+ deadline = time.monotonic() + timeout
170
+ while True:
171
+ scan = self.get_scan(site_id, scan_id)
172
+ if scan is None:
173
+ raise LiveAPISecError(None, "Scan not found", f"scan {scan_id} on site {site_id}")
174
+ status = scan.get("status")
175
+ if status in (ScanStatus.COMPLETED, ScanStatus.FAILED):
176
+ scan["findings"] = self.get_findings(site_id, scan_id)
177
+ return scan
178
+ if time.monotonic() > deadline:
179
+ raise LiveAPISecError(
180
+ None, "Timeout", f"scan {scan_id} still {status!r} after {timeout:.0f}s"
181
+ )
182
+ time.sleep(poll_interval)
183
+
184
+ @staticmethod
185
+ def findings_above(findings: list[dict[str, Any]], min_severity: str) -> list[dict[str, Any]]:
186
+ """Findings o severity >= min_severity (wg ranku: critical < high < ...)."""
187
+ threshold = severity_rank(min_severity)
188
+ return [f for f in findings if severity_rank(f.get("severity", "info")) <= threshold]
189
+
190
+ @staticmethod
191
+ def dump(data: Any) -> str:
192
+ return json.dumps(data, indent=2, ensure_ascii=False, default=str)
@@ -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,8 @@
1
+ liveapisec/__init__.py,sha256=N1gXj7YighntKJMgf9MwRQXe6R1uFRKeStqWqfbkvmA,659
2
+ liveapisec/cli.py,sha256=1PoXrUvfuF2fAW_8dJrGmr8K8pB1ssCTbyz46Yz6et0,11074
3
+ liveapisec/client.py,sha256=WkJLwadR-cCobUIn7YVOYFy2T9g1M7qatf8jCJFXyuY,7239
4
+ liveapisec-0.1.0.dist-info/METADATA,sha256=oqaStjDo5L9zWXX2mgBr96f_UyA8W_uYKZyqamq9Beg,5636
5
+ liveapisec-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ liveapisec-0.1.0.dist-info/entry_points.txt,sha256=g_b-0QQOXSBVpGrlee-81X9TzMvE9_2P7ToBuikjcVo,51
7
+ liveapisec-0.1.0.dist-info/top_level.txt,sha256=JvGd8EoaUOP0UCTIYKrxzsTBwPe40OtwWycgM2sbUYc,11
8
+ liveapisec-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ liveapisec = liveapisec.cli:main
@@ -0,0 +1 @@
1
+ liveapisec