Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,13 @@ jobs:
with:
python-version: "3.13"
cache: pip
- run: pip install -r requirements.txt ruff bandit[toml]
# PINNED DELIBERATELY. Unpinned linters made this job fail with no code
# change: a newer ruff widened F401 and flagged 7 pre-existing unused
# imports. Selecting rules in ruff.toml is not enough - a new release can
# still change what an existing rule detects. Bump these on purpose.
- run: pip install -r requirements.txt ruff==0.16.1 "bandit[toml]==1.9.4"
- name: Lint
# ruff.toml selects the rule set explicitly so a new ruff release
# cannot redefine what 'passing' means. The version is printed so the
# exact linter is recoverable from the log and can be pinned the day
# this project needs bit-reproducible CI.
# The version is printed so the exact linter is recoverable from the log.
run: |
ruff --version
ruff check --config ruff.toml sentinelai tests
Expand Down Expand Up @@ -203,11 +204,13 @@ jobs:
fi
echo "fails closed without a signing secret, as designed"
- name: Trivy filesystem scan
# Pinned to a tag Dependabot has confirmed exists. 0.24.0 stopped
# resolving, which fails the job in 'Set up job' before any step
# runs - a two-second red cross that looks like a build failure
# and is really a missing action reference.
uses: aquasecurity/trivy-action@0.36.0
# NOTE THE 'v'. aquasecurity/trivy-action publishes v-prefixed tags
# (v0.36.0); the bare form (0.36.0, 0.24.0) does not resolve. An
# unresolvable action fails in 'Set up job' before any step runs - a
# two-second red cross that looks like a build failure and is really a
# missing action reference. Verify with:
# gh api repos/aquasecurity/trivy-action/tags --jq '.[0:5][].name'
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY sentinelai/ ./sentinelai/
COPY tests/ ./tests/
COPY README.md ./
# tests/test_openapi_parity.py asserts the committed spec equals the generated
# one, so the in-image suite needs it. Without this the build fails with
# FileNotFoundError on /app/docs/openapi.json - a test that passes on the runner
# and only fails inside the image.
COPY docs/openapi.json ./docs/openapi.json

# The image is only published if its own test suite passes inside the image.
# A container that cannot prove its model code works is not a release artifact.
Expand Down
4 changes: 2 additions & 2 deletions sentinelai/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple

from . import obs

Expand Down
2 changes: 1 addition & 1 deletion sentinelai/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def from_env(cls, environ: Optional[Mapping[str, str]] = None) -> "Settings":

# Binding a security service to every interface is a deliberate act, so
# it must be a deliberate act in production rather than a leftover.
if app_env == "prod" and host == "0.0.0.0" and env.get("SENTINELAI_ALLOW_PUBLIC_BIND") != "1":
if app_env == "prod" and host == "0.0.0.0" and env.get("SENTINELAI_ALLOW_PUBLIC_BIND") != "1": # nosec B104 - this IS the guard that refuses the public bind
problems.append(
"refusing to bind 0.0.0.0 in prod without SENTINELAI_ALLOW_PUBLIC_BIND=1"
)
Expand Down
2 changes: 1 addition & 1 deletion sentinelai/hunt.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import argparse
import json
import re
from dataclasses import dataclass, field as dc_field
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence

Expand Down
4 changes: 2 additions & 2 deletions sentinelai/space_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _proxy(self) -> None:
req.add_header(header, value)

try:
with urllib.request.urlopen(req, timeout=30) as upstream_response:
with urllib.request.urlopen(req, timeout=30) as upstream_response: # nosec B310 - upstream is a hardcoded http://127.0.0.1 loopback URL
status = upstream_response.status
payload = upstream_response.read()
headers = upstream_response.headers.items()
Expand Down Expand Up @@ -153,7 +153,7 @@ def main() -> None:

ConsoleHandler.upstream = f"http://127.0.0.1:{args.api_port}"
handler = partial(ConsoleHandler, directory=str(dist))
public = ThreadingHTTPServer(("0.0.0.0", args.port), handler)
public = ThreadingHTTPServer(("0.0.0.0", args.port), handler) # nosec B104 - the console server binds all interfaces by design (container/Space)
print(f"console on http://0.0.0.0:{args.port} (serving {dist})", flush=True)
try:
public.serve_forever()
Expand Down
12 changes: 6 additions & 6 deletions sentinelai/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import threading
import time
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
from typing import Any, Dict, List, Optional, Tuple

__all__ = [
"Store", "StoreError", "ConflictError", "NotFound", "InvalidTransition",
Expand Down Expand Up @@ -329,7 +329,7 @@ def migrate(self) -> int:
# inlined for the same reason: executescript takes no parameters.
# Every value interpolated here is a module constant, never input.
bookkeeping = (
"INSERT INTO schema_migrations(version, name, applied_at) VALUES ("
"INSERT INTO schema_migrations(version, name, applied_at) VALUES (" # nosec B608 - version is int()-cast, name is ''-escaped; both module constants
+ str(int(version)) + ", '" + name.replace("'", "''") + "', '"
+ _now() + "');")
script = "BEGIN IMMEDIATE;" + sql + bookkeeping + "COMMIT;"
Expand Down Expand Up @@ -398,7 +398,7 @@ def list_alerts(self, limit: int = 50, cursor: Optional[str] = None,
where.append("(probability < ? OR (probability = ? AND id < ?))")
params.extend([float(last_p), float(last_p), last_id])
clause = (" WHERE " + " AND ".join(where)) if where else ""
sql = ("SELECT * FROM alerts" + clause
sql = ("SELECT * FROM alerts" + clause # nosec B608 - clause joins static predicates; all values bound via ?
+ " ORDER BY probability DESC, id DESC LIMIT ?")
params.append(limit + 1)
rows = _rows(self.connect().execute(sql, params))
Expand Down Expand Up @@ -526,7 +526,7 @@ def list_cases(self, limit: int = 50, cursor: Optional[str] = None,
where.append("(updated_at < ? OR (updated_at = ? AND id < ?))")
params.extend([last_ts, last_ts, last_id])
clause = (" WHERE " + " AND ".join(where)) if where else ""
sql = ("SELECT * FROM cases" + clause
sql = ("SELECT * FROM cases" + clause # nosec B608 - clause joins static predicates; all values bound via ?
+ " ORDER BY updated_at DESC, id DESC LIMIT ?")
params.append(limit + 1)
rows = _rows(self.connect().execute(sql, params))
Expand Down Expand Up @@ -624,7 +624,7 @@ def audit_page(self, limit: int = 100, cursor: Optional[str] = None) -> Dict[str
params.append(int(last_seq))
params.append(limit + 1)
rows = _rows(self.connect().execute(
"SELECT * FROM audit_log" + clause + " ORDER BY seq DESC LIMIT ?", params))
"SELECT * FROM audit_log" + clause + " ORDER BY seq DESC LIMIT ?", params)) # nosec B608 - clause joins static predicates; all values bound via ?
next_cursor = None
if len(rows) > limit:
rows = rows[:limit]
Expand Down Expand Up @@ -654,7 +654,7 @@ def fetch_events(self, consumer: str, topic: Optional[str] = None,
params.append(topic)
params.append(int(limit))
return _rows(conn.execute(
"SELECT * FROM outbox WHERE id > ?" + clause + " ORDER BY id LIMIT ?",
"SELECT * FROM outbox WHERE id > ?" + clause + " ORDER BY id LIMIT ?", # nosec B608 - clause joins static predicates; all values bound via ?
params))

def commit_offset(self, consumer: str, last_id: int) -> None:
Expand Down
6 changes: 3 additions & 3 deletions sentinelai/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import threading
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional

from . import obs
from .bus import Bus
Expand Down Expand Up @@ -187,12 +187,12 @@ def job_retention(self) -> Dict[str, Any]:
if names:
placeholders = ",".join("?" for _ in names)
row = conn.execute(
"SELECT MIN(last_id) FROM consumer_offsets WHERE consumer IN ("
"SELECT MIN(last_id) FROM consumer_offsets WHERE consumer IN (" # nosec B608 - placeholders is a join of '?'; names bound as params
+ placeholders + ")", names).fetchone()
committed = row[0] if row is not None else None
# A registered consumer with no offset row has committed nothing.
if committed is not None and len(names) == int(conn.execute(
"SELECT COUNT(*) FROM consumer_offsets WHERE consumer IN ("
"SELECT COUNT(*) FROM consumer_offsets WHERE consumer IN (" # nosec B608 - placeholders is a join of '?'; names bound as params
+ placeholders + ")", names).fetchone()[0]):
safe_below = int(committed)
cutoff = time.strftime(
Expand Down
1 change: 0 additions & 1 deletion tests/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""

import json
import os
import sqlite3
import tempfile
import threading
Expand Down
Loading