Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main, master]
branches: [main, master, "release/**"]
pull_request:
branches: [main, master]
branches: [main, master, "release/**"]

jobs:
test:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

**Scoped authorization for AI-shaped scientific work**

[![Version](https://img.shields.io/badge/version-0.8.1-blue)](https://github.com/fraware/SCOPE/releases)
[![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/fraware/SCOPE/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![CI](https://github.com/fraware/SCOPE/actions/workflows/ci.yml/badge.svg)](https://github.com/fraware/SCOPE/actions/workflows/ci.yml)
Expand Down
47 changes: 29 additions & 18 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,54 @@

| Version | Supported | Notes |
| ------- | --------- | ----- |
| 0.8.x | Yes | Current release line; security fixes backported here |
| 0.7.x | Best effort | Prior institutional pilot line; upgrade to 0.8.x recommended |
| 0.6.x and earlier | No | Unsupported; no security patches |
| 1.0.x | Yes | Current stable line; contract freeze |
| 0.11.x | Yes | Workflow release; security fixes backported when feasible |
| 0.10.x | Best effort | Production trust line; upgrade to 1.0.x recommended |
| 0.9.x | Best effort | Ecosystem demo line |
| 0.8.x | Best effort | Prior release line |
| 0.7.x and earlier | No | Unsupported |

Report issues against the latest **0.8.x** release on [main](https://github.com/fraware/SCOPE).
Report issues against the latest **1.0.x** release on [main](https://github.com/fraware/SCOPE).

## Reporting a vulnerability

Report security issues privately via GitHub Security Advisories on [fraware/SCOPE](https://github.com/fraware/SCOPE/security/advisories/new). Do not open public issues for undisclosed vulnerabilities.

## v0.8 security model
## v1.0 security model

SCOPE v0.8.x builds on schema validation, canonical hashing, hash-chained ledger events, explicit expiration checks, and fail-closed behavior for unknown scopes, invalid roles, and forbidden queue transitions.
SCOPE v1.0 builds on schema validation, canonical hashing, hash-chained ledger events, explicit expiration checks, and fail-closed behavior for unknown scopes, invalid roles, and forbidden queue transitions.

**Cryptography and signing**

- Ed25519 signatures on decisions and grants when production signing is enabled (`SCOPE_PRODUCTION_MODE`, minimum signing assurance policy in `policy/minimum_signing_assurance.yaml`)
- Signing assurance levels (SAL0–SAL4) with registry key binding and reviewer public-key references; external HSM/KMS interface documented for SAL4 (`docs/signing_assurance.md`, `docs/key_management.md`)
- Combined `scope_trust_root_hash` ties policy and reviewer registry integrity into decision, grant, and PCS export provenance
- Ed25519 signatures on decisions and grants when production signing is enabled
- Signing assurance levels (SAL0–SAL4) with KMS reference adapter (`--signing-provider kms`)
- Combined `scope_trust_root_hash` ties policy and reviewer registry integrity into PCS export provenance

**Identity**

- Identity assurance levels (IAL0–IAL4) with provenance on decisions and session grants (`scope/identity_assurance.py`, `docs/identity_assurance.md`)
- Optional OIDC/JWT verification (`SCOPE_OIDC_*`, `scope identity verify-token`) for institutional identity claims; org RBAC in `policy/org_rbac.yaml` is separate from SCOPE scope authority
- Identity assurance levels (IAL0–IAL4); production mode rejects IAL0 unless `SCOPE_ALLOW_DEV_IAL0`
- Pluggable OIDC and SAML providers (`scope/identity_providers.py`)
- SCIM/LDAP RBAC sync via `scope rbac sync`

**Ledger and delivery**

- Local hash-chained JSONL ledger with verification APIs
- Optional remote HTTP append sink (`SCOPE_LEDGER_REMOTE_URL`); delivery semantics `best_effort`, `at_least_once` (spool), and `fail_closed` for high-risk grant issuance when remote delivery is required
- Runtime violation and expiration events for PF feedback loops; remote sink is not a WORM or authoritative tamper-evident store
- Local hash-chained JSONL ledger with WORM and verified remote sink options
- Delivery modes: `best_effort`, `at_least_once`, `fail_closed`
- REST API audit logging to ledger

**AKTA review contract**

- Signed `summary.json` artifacts validated against split schemas for `completed` vs `session_required` (`scope-akta-review-v0.8.1`); consumers must branch on `summary.status`
- Frozen at `scope-akta-review-v1.0`; consumers branch on `summary.status`

**Workflow**

- Tenant-isolated review queues via `X-Scope-Tenant-Id`
- Webhook notifications on SLA breach

**Known limits**

- No live SAML/SCIM directory sync; RBAC and identity mapping are file-based
- Reviewer judgment, domain safety, and physical lab safety are out of scope
- See [docs/threat_model.md](docs/threat_model.md), [docs/trusted_boundary.md](docs/trusted_boundary.md), and [docs/limitations.md](docs/limitations.md) for residual risk and deployment boundaries.
- SAML verification requires external sidecar or pre-verified assertions in reference adapter
- Email notifications require institutional SMTP wiring
- Reviewer judgment and physical lab safety remain out of scope

See [docs/threat_model.md](docs/threat_model.md), [docs/trusted_boundary.md](docs/trusted_boundary.md), [docs/compatibility_matrix.md](docs/compatibility_matrix.md), and [docs/limitations.md](docs/limitations.md).
41 changes: 39 additions & 2 deletions adapters/generic_rest/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,38 @@ def reset_engine_cache() -> None:
async def _scope_request_context(request: Request, call_next): # type: ignore[no-untyped-def]
token = _request_context.set(request)
try:
return await call_next(request)
response = await call_next(request)
_audit_rest_request(request, response.status_code)
return response
finally:
_request_context.reset(token)


def _audit_rest_request(request: Request, status_code: int) -> None:
"""Append REST API audit event to ledger when enabled."""
if os.environ.get("SCOPE_REST_AUDIT", "true").lower() in ("0", "false", "no"):
return
if request.url.path in ("/docs", "/openapi.json", "/redoc"):
return
try:
engine = get_engine(request)
caller_hdr = request.headers.get("x-scope-caller-id")
caller = caller_hdr or (request.client.host if request.client else "unknown")
tenant = request.headers.get("x-scope-tenant-id")
engine.ledger.append(
"rest_api_audit",
metadata={
"method": request.method,
"path": request.url.path,
"status_code": status_code,
"caller": caller,
"tenant_id": tenant,
},
)
except Exception:
pass


def _signer_from_env(explicit: str | None = None) -> Ed25519Signer:
key_path = explicit or os.environ.get("SCOPE_SIGNING_KEY")
if not key_path:
Expand Down Expand Up @@ -217,6 +244,8 @@ class AktaReviewRequest(BaseModel):
identity_token: str | None = None
queue_dir: str | None = None
session_mode: bool = False
session_complete: bool = False
votes: list[dict[str, Any]] | None = None


def _http_error(exc: Exception) -> HTTPException:
Expand Down Expand Up @@ -407,6 +436,8 @@ def akta_review(req: AktaReviewRequest) -> dict[str, Any]:
identity_token=req.identity_token,
queue_dir=req.queue_dir,
session_mode=req.session_mode,
session_complete=req.session_complete,
votes=req.votes,
)
return summary
except HTTPException:
Expand All @@ -433,9 +464,15 @@ def list_review_queue(queue_dir: str | None = None) -> dict[str, Any]:


def _find_queue_path(queue_id: str, queue_dir: str | None = None) -> Path:
from scope.errors import ScopeValidationError
from scope.review_queue import ReviewQueue, list_queue_files

for path in list_queue_files(queue_dir):
engine = get_engine()
try:
effective = engine.effective_queue_dir(queue_dir)
except ScopeValidationError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
for path in list_queue_files(effective):
entry = ReviewQueue.load(path)
if entry.queue_id == queue_id:
return path
Expand Down
Empty file added adapters/workflow/__init__.py
Empty file.
63 changes: 63 additions & 0 deletions adapters/workflow/jira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Minimal Jira ticket adapter for review queue sync."""

from __future__ import annotations

import json
import os
import urllib.error
import urllib.request
from typing import Any, cast


def _jira_base() -> str:
base = os.environ.get("SCOPE_JIRA_URL")
if not base:
raise ValueError("SCOPE_JIRA_URL not configured")
return base.rstrip("/")


def _auth_header() -> dict[str, str]:
token = os.environ.get("SCOPE_JIRA_TOKEN")
if not token:
raise ValueError("SCOPE_JIRA_TOKEN not configured")
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}


def create_ticket(queue_summary: dict[str, Any]) -> dict[str, Any]:
"""Create Jira issue from queue state."""
project = os.environ.get("SCOPE_JIRA_PROJECT", "SCOPE")
payload = {
"fields": {
"project": {"key": project},
"summary": f"SCOPE review {queue_summary.get('queue_id')}",
"description": json.dumps(queue_summary, indent=2),
"issuetype": {"name": "Task"},
}
}
req = urllib.request.Request(
f"{_jira_base()}/rest/api/3/issue",
data=json.dumps(payload).encode("utf-8"),
headers=_auth_header(),
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return cast(dict[str, Any], json.loads(resp.read().decode("utf-8")))


def update_ticket(ticket_id: str, queue_summary: dict[str, Any]) -> dict[str, Any]:
"""Update existing Jira issue with queue state."""
payload = {
"fields": {
"description": json.dumps(queue_summary, indent=2),
}
}
req = urllib.request.Request(
f"{_jira_base()}/rest/api/3/issue/{ticket_id}",
data=json.dumps(payload).encode("utf-8"),
headers=_auth_header(),
method="PUT",
)
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status == 204:
return {"id": ticket_id, "updated": True}
return cast(dict[str, Any], json.loads(resp.read().decode("utf-8")))
57 changes: 57 additions & 0 deletions adapters/workflow/servicenow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Minimal ServiceNow ticket adapter for review queue sync."""

from __future__ import annotations

import json
import os
import urllib.error
import urllib.request
from typing import Any, cast


def _snow_base() -> str:
base = os.environ.get("SCOPE_SERVICENOW_URL")
if not base:
raise ValueError("SCOPE_SERVICENOW_URL not configured")
return base.rstrip("/")


def _auth_header() -> dict[str, str]:
token = os.environ.get("SCOPE_SERVICENOW_TOKEN")
if not token:
raise ValueError("SCOPE_SERVICENOW_TOKEN not configured")
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}


def create_ticket(queue_summary: dict[str, Any]) -> dict[str, Any]:
"""Create ServiceNow incident from queue state."""
table = os.environ.get("SCOPE_SERVICENOW_TABLE", "incident")
payload = {
"short_description": f"SCOPE review {queue_summary.get('queue_id')}",
"description": json.dumps(queue_summary, indent=2),
"urgency": "2",
}
req = urllib.request.Request(
f"{_snow_base()}/api/now/table/{table}",
data=json.dumps(payload).encode("utf-8"),
headers=_auth_header(),
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
return cast(dict[str, Any], data.get("result", data))


def update_ticket(ticket_id: str, queue_summary: dict[str, Any]) -> dict[str, Any]:
"""Update ServiceNow record with queue state."""
table = os.environ.get("SCOPE_SERVICENOW_TABLE", "incident")
payload = {"description": json.dumps(queue_summary, indent=2)}
req = urllib.request.Request(
f"{_snow_base()}/api/now/table/{table}/{ticket_id}",
data=json.dumps(payload).encode("utf-8"),
headers=_auth_header(),
method="PUT",
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
return cast(dict[str, Any], data.get("result", data))
14 changes: 10 additions & 4 deletions docs/akta_review_contract.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# AKTA Review Output Contract

SCOPE v0.8.1 splits the `scope akta review` output contract by `summary.status`.
SCOPE v1.0 splits the `scope akta review` output contract by `summary.status`.

## Contract version

**Frozen at v1.0:** `scope-akta-review-v1.0` (`scope.integration_versions.AKTA_REVIEW_CONTRACT_VERSION`).

Backward compatible with v0.8.1 and v0.9 consumers when branching on `summary.status`.

## Branching rule

Expand All @@ -13,7 +19,7 @@ Consumers **must** branch on `summary.status`:

The two schemas are mutually exclusive: completed summaries cannot carry session fields (`session_id`, `required_roles`, `message`); session summaries cannot carry grant/decision fields (`decision_path`, `grant_path`, `approved_scope`, IAL/SAL, etc.).

Contract version constant: `scope-akta-review-v0.8.1` (`scope.integration_versions.AKTA_REVIEW_CONTRACT_VERSION`).
Contract version constant: `scope-akta-review-v1.0` (`scope.integration_versions.AKTA_REVIEW_CONTRACT_VERSION`).

## Artifacts

Expand All @@ -36,7 +42,7 @@ Required fields:
"grant_path": "...",
"approved_scope": "...",
"requested_scope": "...",
"adapter_contract_version": "scope-akta-review-v0.8.1",
"adapter_contract_version": "scope-akta-review-v1.0",
"identity_assurance_level": "IAL0",
"signing_assurance_level": "SAL1",
"production_mode": false
Expand All @@ -60,7 +66,7 @@ Required fields:
"session_id": "SCOPE-SESS-...",
"required_roles": ["domain_scientist", "protocol_owner"],
"message": "Multi-role review session created; submit votes before grant issue.",
"adapter_contract_version": "scope-akta-review-v0.8.1",
"adapter_contract_version": "scope-akta-review-v1.0",
"production_mode": false
}
```
Expand Down
45 changes: 45 additions & 0 deletions docs/compatibility_matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Compatibility Matrix (v1.0)

Published version alignment for SCOPE and partner integrations at v1.0.0.

## Core versions

| Component | Version constant | Notes |
|-----------|------------------|-------|
| SCOPE package | `0.11.0` → `1.0.0` | Semver from `scope/_version.py` |
| Policy bundle | `scope-core-v1.0` | YAML `version` field |
| AKTA review contract | `scope-akta-review-v1.0` | Branch on `summary.status` |
| PF obligation | `pf-core-v0.5` | `obligation_version` |
| PCS manifest | `pcs-v0.5` | `manifest_version` |
| VSA report import | adapter-local | No semver field; schema-stable fields documented |

## Cross-repo compatibility

| SCOPE | AKTA | PF-Core | PCS | VSA |
|-------|------|---------|-----|-----|
| 1.0.x | ≥ 0.4 trigger aliases | pf-core-v0.5 | pcs-v0.5 | ScientificReport v1 |
| 0.11.x | scope-akta-review-v0.9+ | pf-core-v0.5 | pcs-v0.5 | ScientificReport v1 |
| 0.10.x | scope-akta-review-v0.9+ | pf-core-v0.5 | pcs-v0.5 | ScientificReport v1 |
| 0.9.x | scope-akta-review-v0.9 | pf-core-v0.4–v0.5 | pcs-v0.4–v0.5 | ScientificReport v1 |
| 0.8.x | scope-akta-review-v0.8.1 | pf-core-v0.5 | pcs-v0.5 | ScientificReport v1 |

## Schema stability (v1.0 freeze)

Stable fields in `schemas/` will not be removed or change type without a major SCOPE bump. Deprecated fields remain readable for one major release with documented migration paths.

| Schema | Stable since |
|--------|--------------|
| `scope_akta_review_summary.schema.json` | v1.0.0 |
| `scope_akta_review_session_summary.schema.json` | v1.0.0 |
| `pf_scope_obligation.schema.json` | v0.7.0 |
| `pcs_scope_artifact.schema.json` | v0.7.0 |
| `scope_review_queue.schema.json` | v0.7.0 |

## Migration guides

- **v0.8 → v0.9:** Session-complete AKTA path; contract version bump to `scope-akta-review-v0.9`
- **v0.9 → v0.10:** Production IAL0 rejection; KMS signing provider; RBAC SCIM sync
- **v0.10 → v0.11:** Tenant queue isolation; webhook notifications; REST audit logging
- **v0.11 → v1.0:** Contract freeze at `scope-akta-review-v1.0`; no breaking schema changes

See [akta_review_contract.md](akta_review_contract.md), [external_integration_contracts.md](external_integration_contracts.md).
Loading
Loading