# sika-python

[![Tests](https://github.com/sika-security/sika-python/actions/workflows/test.yml/badge.svg)](https://github.com/sika-security/sika-python/actions/workflows/test.yml)
[![Security](https://github.com/sika-security/sika-python/actions/workflows/security.yml/badge.svg)](https://github.com/sika-security/sika-python/actions/workflows/security.yml)

Python SDK and CLI for the [Sika Security](https://sikasecurity.com) platform.

Two packages, one repo:

| Package | PyPI | Purpose |
|---------|------|---------|
| `sika-sdk` | `pip install sika-sdk` | Typed Python client for all Sika product APIs |
| `sika-cli` | `pip install sika-sdk[cli]` | Unified `sika` command-line interface |

## Installation

```bash
# SDK only (for programmatic use)
pip install sika-sdk

# SDK + CLI (for the `sika` command)
pip install sika-sdk[cli]
```

Requires **Python 3.11+**.

## Quick Start

```python
from sika import SikaClient

# Authenticate with an API key
client = SikaClient(api_key="st_recon_...")

# Submit a recon scan
scan = client.recon.scans.create("example.com")
print(f"Scan {scan.scan_id}: {scan.status}")

# Retrieve scan details
scan = client.recon.scans.get(scan.scan_id)
print(f"Status: {scan.status}")

# List all scans
for scan in client.recon.scans.list():
    print(f"{scan.domain} — {scan.status}")

# Wait for scan completion (polls automatically)
scan = client.recon.scans.wait(scan.scan_id, timeout=300, poll_interval=5)
print(f"Completed: {scan.summary}")

# Download a report
report = client.recon.scans.report(scan.scan_id, format="json")
print(f"Download: {report.url}")

# Check service health
health = client.recon.health()
print(f"Service: {health.status}, version: {health.version}")

# Intel database status
intel = client.recon.intel.status()
print(f"Intel updated: {intel.last_updated}")

# Delete a scan
client.recon.scans.delete(scan.scan_id)
```

### CLI

```bash
# Set your API key
export SIKA_API_KEY="st_recon_..."

# Submit a recon scan
sika recon scan example.com

# Submit and wait for completion
sika recon scan example.com --attach

# List recent scans
sika recon jobs list

# Filter by status
sika recon jobs list --status completed

# View scan details
sika recon jobs get <scan_id>

# Download a report (JSON, HTML, or CSV)
sika recon pull <scan_id>
sika recon pull <scan_id> --format html --output report.html

# Batch scan: submit multiple domains from a file
sika recon scan --file targets.txt

# Batch scan with explicit tag
sika recon scan --file targets.txt --tag engagement-acme

# Tag a single scan
sika recon scan example.com --tag pentest-q1

# JSON output for scripting
sika --json recon jobs list
sika --quiet recon scan example.com  # prints only scan_id

# Authenticate via browser (creates API token, stores in ~/.sika/credentials)
sika auth login

# Check authentication status
sika auth status

# Log out (remove stored credentials)
sika auth logout
```

Global options: `--json`, `--quiet`, `--no-color`, `--profile`, `--env`.

Exit codes: 0 success, 1 error, 2 auth, 3 forbidden/quota, 4 not found,
5 validation, 130 interrupted.

## Authentication

The SDK resolves credentials in this order (highest priority first):

1. **Constructor parameter**: `SikaClient(api_key="...")`
2. **Product-specific env var**: `SIKA_RECON_API_KEY`
3. **Global env var**: `SIKA_API_KEY`
4. **Credential file**: `~/.sika/credentials` (TOML format)

```python
# Explicit key
client = SikaClient(api_key="st_recon_...")

# Or set SIKA_API_KEY in your environment and omit api_key
client = SikaClient()
```

### Credential file

The credential file uses TOML format and supports named profiles:

```toml
[default]
api_key = "st_recon_..."

[profiles.staging]
api_key = "st_recon_..."
```

Use a named profile:

```python
client = SikaClient(profile="staging")
```

The credential file must have `0600` permissions (owner read/write only).
The SDK warns if the file is group- or world-readable.

## Error Handling

The SDK maps API error responses ([RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807)
Problem Details) to a typed exception hierarchy. All exceptions inherit
from `SikaError` and carry the HTTP status code, machine-readable error
code, and human-readable detail message.

```python
from sika import (
    SikaClient,
    SikaError,
    AuthenticationError,
    NotFoundError,
    QuotaExceededError,
    ValidationError,
)

client = SikaClient(api_key="st_recon_...")

try:
    scan = client.recon.scans.get("nonexistent-id")
except NotFoundError as e:
    print(f"Not found: {e.detail}")
except AuthenticationError:
    print("Check your API key")
except QuotaExceededError as e:
    print(f"Quota exceeded: {e.current}/{e.limit} {e.dimension}")
    if e.upgrade_url:
        print(f"Upgrade at: {e.upgrade_url}")
except ValidationError as e:
    print(f"Invalid request: {e.detail}")
    for field_error in e.errors:
        print(f"  - {field_error}")
except SikaError as e:
    print(f"API error [{e.status_code}]: {e.detail}")
```

Exception hierarchy: see `docs/sdk-architecture.md` Section 7.

## Pagination

List endpoints return a `PageIterator` that auto-fetches pages as you
iterate. No manual cursor management required.

```python
# Iterate over all items (pages fetched lazily)
for scan in client.recon.scans.list():
    print(scan.domain)

# Filter by status
from sika.recon.models import ScanStatus

for scan in client.recon.scans.list(status=ScanStatus.completed):
    print(f"{scan.domain} completed at {scan.completed_at}")

# Iterate page-by-page
for page in client.recon.scans.list().pages():
    print(f"{page.count} items, next_token={page.next_token}")
    for scan in page.items:
        print(scan.domain)
```

A safety limit of 10,000 items prevents unbounded iteration. The
`PageIterator` accepts a `max_items` parameter to override this.

See `docs/sdk-architecture.md` Section 8 for details.

## Development

```bash
make venv       # Create virtualenv and install dev dependencies
make lint       # Run ruff + mypy
make format     # Auto-format with ruff
make test       # Run unit tests
make check      # lint + test (pre-push gate)
make build      # Build sdist + wheel
make clean      # Remove venv, build artifacts, caches
make ci-status  # Wait for GitHub Actions and report pass/fail
```

### Releasing

Releases are published to PyPI automatically when a version tag is pushed:

```bash
# Update version in src/sika/_version.py, then:
git tag v0.1.0
git push origin v0.1.0
```

The `publish` workflow builds sdist + wheel and publishes via
[trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC).
No API tokens required.

## Project Structure

```
src/sika/           SDK package (published as sika-sdk)
cli/sika_cli/       CLI package (published as sika-sdk[cli])
tests/unit/         Unit tests (mirrors source tree)
tests/integration/  Integration tests (live API)
docs/               Architecture docs
pm/                 Roadmap, active plan, dev processes
```

## License

MIT
