Skip to content

Commit d42e730

Browse files
feat: add ruff and pip-audit CI gates, fix style violations, and add SECURITY.md
1 parent 6952d6d commit d42e730

27 files changed

Lines changed: 211 additions & 79 deletions

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ jobs:
4343
assert client.get("/").status_code == 200
4444
PY
4545
46+
lint-and-audit:
47+
name: ruff + pip-audit (${{ matrix.os }})
48+
runs-on: ${{ matrix.os }}
49+
strategy:
50+
fail-fast: false
51+
matrix:
52+
os: [ubuntu-latest, windows-latest]
53+
permissions:
54+
contents: read
55+
steps:
56+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
57+
with:
58+
persist-credentials: false
59+
60+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
61+
with:
62+
python-version: "3.12"
63+
cache: pip
64+
cache-dependency-path: |
65+
requirements.txt
66+
requirements-dev.txt
67+
68+
- name: Install dev dependencies
69+
run: pip install -r requirements-dev.txt
70+
71+
- name: Ruff
72+
run: ruff check .
73+
74+
- name: pip-audit
75+
run: pip-audit -r requirements.txt
76+
4677
pytest:
4778
name: pytest (${{ matrix.os }})
4879
runs-on: ${{ matrix.os }}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ REST endpoints for projects, sessions, search, and export are documented in **[`
2727

2828
JSON error responses include a machine-readable `"code"` (stable `UPPER_SNAKE_CASE`) and a human-readable `"error"` message. See the [error code catalog](docs/api-reference.md#error-code-catalog) for the full table.
2929

30+
- **Security policy** — see [`SECURITY.md`](SECURITY.md) for supported versions and how to report vulnerabilities privately
31+
3032
### CLI Export
3133
- Standalone script to export all sessions to Markdown with YAML frontmatter
3234
- Rich Markdown: token usage, tool calls, thinking blocks, model info, timestamps
@@ -155,7 +157,7 @@ npm ci && npm test # only if you changed static/js/
155157

156158
## Continuous integration
157159

158-
Every push and pull request runs **`pytest`**, **API integration tests**, and **vitest** on **Ubuntu** (Python 3.12, Node 20) via [`.github/workflows/ci.yml`](.github/workflows/ci.yml). A separate job verifies that `pip install -r requirements.txt` (production-only) is sufficient to import and boot the app.
160+
Every push and pull request runs **`ruff`**, **`pip-audit`**, **`pytest`**, **mypy**, **API integration tests**, and **vitest** on **Ubuntu and Windows** (Python 3.12, Node 20) via [`.github/workflows/ci.yml`](.github/workflows/ci.yml). A separate job verifies that `pip install -r requirements.txt` (production-only) is sufficient to import and boot the app.
159161

160162
## Exported Markdown Format
161163

SECURITY.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Security Policy
2+
3+
## Supported Versions
4+
5+
This project is pre-release. Security fixes are applied to the **latest `master` branch only** (currently `0.1.0.dev0`).
6+
7+
| Version | Supported |
8+
| -------------- | --------- |
9+
| latest `master`| Yes |
10+
| older commits | No |
11+
12+
## Reporting a Vulnerability
13+
14+
**Please do not open public GitHub issues for security vulnerabilities.**
15+
16+
Report vulnerabilities privately via [GitHub Security Advisories](https://github.com/cppalliance/claude-code-chat-browser/security/advisories/new). Private vulnerability reporting must be enabled on the repository (Settings → Security → Private vulnerability reporting). If you cannot use that form, contact the repository maintainers through an existing private channel.
17+
18+
## Response Timeline
19+
20+
| Stage | Target |
21+
| ----- | ------ |
22+
| Acknowledgment | Within **72 hours** of a valid report |
23+
| Initial assessment | Within **7 days** |
24+
| Fix for confirmed issues | Target **14 days** for issues affecting the default local-only deployment |
25+
26+
Timelines may extend for complex issues; we will keep reporters informed.
27+
28+
## Scope
29+
30+
### In scope
31+
32+
Security issues in this repository that affect users of the default local setup:
33+
34+
- **Path traversal** — session and export paths resolved via `safe_join` in `utils/session_path.py`
35+
- **Cross-site scripting (XSS)** — rendered session HTML in `static/js/` (mitigated by DOMPurify + SRI in `static/index.html`)
36+
- **Export integrity** — bulk zip and per-session export in `api/export_api.py` and `utils/export_engine.py`
37+
- **Local file boundaries** — read-only access to `~/.claude/projects/`; writes limited to export output and app state
38+
- **Debug-mode exposure** — Flask/Werkzeug debugger when `--debug` is combined with a non-loopback `--host` (blocked at startup in `app.py`)
39+
- **Information disclosure** — API error responses scrub internal exception details (see `api/error_codes.py`)
40+
41+
### Out of scope
42+
43+
- **Intentional network-facing deployment** — this tool is designed for local browsing on loopback; exposing it on untrusted networks is not a supported configuration
44+
- **Upstream Claude Code JSONL format bugs** — malformed or hostile data from Claude Code itself (we harden parsing but do not guarantee full isolation from arbitrary JSONL)
45+
- **Third-party CDN availability** — DOMPurify is loaded from cdnjs with SRI; CDN compromise is an infrastructure concern outside this repo
46+
47+
## Existing Controls (reference)
48+
49+
| Control | Location |
50+
| ------- | -------- |
51+
| Path guard (`safe_join`) | `utils/session_path.py` |
52+
| HTML sanitization (DOMPurify) | `static/js/shared/markdown.js`, `static/index.html` |
53+
| Error response scrubbing | `api/error_codes.py`, session card handling in `api/projects.py` |
54+
| Debug + non-loopback host guard | `app.py` (`validate_startup_cli`) |

api/export_api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Export endpoints -- bulk zip download and single-session md/json."""
22

33
import io
4-
import json
54
import os
65
import zipfile
76
from datetime import datetime
@@ -12,20 +11,21 @@
1211
from api._flask_types import FlaskReturn, json_response
1312
from api.error_codes import ErrorCode, error_response
1413
from models.export import ExportStateDict
14+
from utils.exclusion_rules import is_session_excluded
15+
from utils.export_engine import EXPORT_ERRORS as _EXPORT_ERRORS
16+
from utils.export_engine import ZipSink, run_bulk_export
1517
from utils.export_state_store import (
1618
EXPORT_STATE_FILE,
1719
atomic_write_export_state,
1820
export_state_lock,
1921
load_export_state_from_disk,
2022
)
21-
from utils.session_path import get_claude_projects_dir, list_projects
23+
from utils.json_exporter import session_to_json
2224
from utils.jsonl_parser import parse_session
23-
from utils.exclusion_rules import is_session_excluded
24-
from utils.session_stats import compute_stats
2525
from utils.md_exporter import session_to_markdown
26-
from utils.json_exporter import session_to_json
26+
from utils.session_path import get_claude_projects_dir, list_projects
27+
from utils.session_stats import compute_stats
2728
from utils.slugify import slugify
28-
from utils.export_engine import EXPORT_ERRORS as _EXPORT_ERRORS, ZipSink, run_bulk_export
2929

3030
export_bp = Blueprint("export", __name__)
3131

api/projects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
from flask import Blueprint, current_app
44

5-
from api._flask_types import FlaskReturn, json_error, json_response
5+
from api._flask_types import FlaskReturn, json_response
66
from api.error_codes import ErrorCode, error_response
77
from models.project import ProjectSessionRowDict, SessionListItemDict
88
from models.session import SessionDict
9-
from utils.session_path import get_claude_projects_dir, list_projects, list_sessions, safe_join
109
from utils.exclusion_rules import is_session_excluded
10+
from utils.session_path import get_claude_projects_dir, list_projects, list_sessions, safe_join
1111

1212
projects_bp = Blueprint("projects", __name__)
1313

api/search.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"""Search endpoint. Brute-force substring match across all sessions."""
22

3-
import os
43

54
from flask import Blueprint, current_app, request
65

76
from api._flask_types import FlaskReturn, json_response
87
from api.error_codes import ErrorCode, error_response
98
from models.search import SearchHitDict
10-
from utils.session_path import get_claude_projects_dir, list_projects, list_sessions
11-
from utils.jsonl_parser import parse_session
129
from utils.exclusion_rules import is_session_excluded
10+
from utils.jsonl_parser import parse_session
11+
from utils.session_path import get_claude_projects_dir, list_projects, list_sessions
1312

1413
search_bp = Blueprint("search", __name__)
1514

api/sessions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88
from api._flask_types import FlaskReturn, json_response
99
from api.error_codes import ErrorCode, error_response
10-
from utils.session_path import get_claude_projects_dir, safe_join
10+
from utils.exclusion_rules import is_session_excluded
1111
from utils.jsonl_parser import parse_session
12+
from utils.session_path import get_claude_projects_dir, safe_join
1213
from utils.session_stats import compute_stats
13-
from utils.exclusion_rules import is_session_excluded
1414

1515
sessions_bp = Blueprint("sessions", __name__)
1616

app.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@
33
__version__ = "0.1.0.dev0"
44

55
import argparse
6-
import os
76
import sys
87

98
from flask import Flask
109

10+
from api.export_api import export_bp
1111
from api.projects import projects_bp
12-
from api.sessions import sessions_bp
1312
from api.search import search_bp
14-
from api.export_api import export_bp
15-
from utils.exclusion_rules import resolve_exclusion_rules_path, load_rules
13+
from api.sessions import sessions_bp
14+
from utils.exclusion_rules import load_rules, resolve_exclusion_rules_path
1615

1716

1817
def _normalize_bind_host(host: str) -> str:
@@ -100,7 +99,10 @@ def build_cli_parser() -> argparse.ArgumentParser:
10099
"--debug",
101100
action="store_true",
102101
default=False,
103-
help="Enable Flask/Werkzeug debug mode (never use with --host 0.0.0.0 on untrusted networks).",
102+
help=(
103+
"Enable Flask/Werkzeug debug mode "
104+
"(never use with --host 0.0.0.0 on untrusted networks)."
105+
),
104106
)
105107
parser.add_argument("--base-dir", default=None, help="Override Claude projects dir")
106108
parser.add_argument(

models/export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Export state file shapes."""
22

3-
from typing import NotRequired, TypedDict
3+
from typing import TypedDict
44

55

66
class ExportStateDict(TypedDict, total=False):

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,16 @@ omit = [
1616
[tool.coverage.report]
1717
fail_under = 60
1818
include = ["api/*", "utils/*"]
19+
20+
[tool.ruff]
21+
target-version = "py312"
22+
line-length = 100
23+
24+
[tool.ruff.lint]
25+
select = ["E", "F", "W", "I"]
26+
27+
[tool.ruff.lint.per-file-ignores]
28+
# CLI bootstrap: sys.path must be set before local imports.
29+
"scripts/export.py" = ["E402"]
30+
# Tests mirror the same path bootstrap before importing app/utils.
31+
"tests/*.py" = ["E402"]

0 commit comments

Comments
 (0)