Add new features and tests for AirGap and Time modules
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Introduced `SbomService` tasks documentation. - Updated `StellaOps.sln` to include new projects: `StellaOps.AirGap.Time` and `StellaOps.AirGap.Importer`. - Added unit tests for `BundleImportPlanner`, `DsseVerifier`, `ImportValidator`, and other components in the `StellaOps.AirGap.Importer.Tests` namespace. - Implemented `InMemoryBundleRepositories` for testing bundle catalog and item repositories. - Created `MerkleRootCalculator`, `RootRotationPolicy`, and `TufMetadataValidator` tests. - Developed `StalenessCalculator` and `TimeAnchorLoader` tests in the `StellaOps.AirGap.Time.Tests` namespace. - Added `fetch-sbomservice-deps.sh` script for offline dependency fetching.
This commit is contained in:
4
docs/modules/findings-ledger/README.md
Normal file
4
docs/modules/findings-ledger/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
# Findings Ledger
|
||||
|
||||
Start here for ledger docs.
|
||||
70
docs/modules/findings-ledger/export-http-surface.md
Normal file
70
docs/modules/findings-ledger/export-http-surface.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Findings Ledger Export HTTP Surface
|
||||
|
||||
Prep task: PREP-LEDGER-EXPORT-35-001-NO-HTTP-API-SURFACE (Sprint 0121)
|
||||
|
||||
## Goals
|
||||
- Publish an HTTP surface for deterministic, offline-friendly exports of Findings Ledger data (findings, VEX, advisories, SBOMs) so downstream SDK/OpenAPI tasks can proceed.
|
||||
- Define filter contract, pagination, media types, and provenance fields required by Evidence Locker and Policy Engine consumers.
|
||||
|
||||
## Non-goals
|
||||
- Implementing the endpoints (covered by LEDGER-EXPORT-35-001).
|
||||
- Final OAS/SDK generation (tracked in PREP-LEDGER-OAS-61-001/002/62-001/63-001).
|
||||
|
||||
## Base Service
|
||||
- Host: findings-ledger service (minimal API) under `src/Findings/StellaOps.Findings.Ledger`.
|
||||
- Base path: `/ledger/export`.
|
||||
- Auth: service-to-service bearer token; require `scope=ledger.export.read`.
|
||||
- Tenancy: `X-Stella-Tenant` header (strictly required); responses never mix tenants.
|
||||
- Determinism: server sorts by `(event_sequence, projection_version, cycle_hash)`; pagination tokens encode the last emitted tuple and filter set. No wall-clock dependence.
|
||||
- Media types: default `application/x-ndjson`; clients may request `application/json` (array) for small result sets. Always emit `Content-Encoding: gzip` when `Accept-Encoding` allows.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1) Findings
|
||||
- `GET /ledger/export/findings`
|
||||
- Filters (all optional unless noted):
|
||||
- `shape` (required): `canonical` \| `compact` (controls payload shape; compact strips verbose provenance fields for air-gap bundles)
|
||||
- `since_sequence` (long, ≥0), `until_sequence` (long, inclusive) — enables range slicing.
|
||||
- `since_observed_at`, `until_observed_at` (ISO-8601 UTC) — observation window.
|
||||
- `advisory_id` (repeatable), `component_purl` (repeatable), `finding_status` (`open|fixed|dismissed`), `severity` (`critical|high|medium|low|unknown`).
|
||||
- `risk_profile_version` (string) — filters by attached risk profile revision.
|
||||
- Response item (canonical shape):
|
||||
- `finding_id`, `event_sequence`, `observed_at`, `component` (purl, version, source), `advisories` (ids, cwes), `status`, `severity`, `risk` (score, severity, profile_version, explanation_id), `projection_version`, `cycle_hash`, `evidence_bundle_ref` (digest, dsse_digest, timeline_ref), `provenance` (ledger_root, projector_version, policy_version, datasource_ids).
|
||||
|
||||
### 2) VEX
|
||||
- `GET /ledger/export/vex`
|
||||
- Filters: `shape`, `since_sequence/until_sequence`, `since_observed_at/until_observed_at`, `product_id` (repeatable), `advisory_id`, `status` (`affected|not_affected|under_investigation`), `statement_type` (`exploitation|justification`).
|
||||
- Response item adds `vex_statement_id`, `product` (purl or CPE), `status_justification`, `known_exploited` (bool), `timestamp`, `projection_version`, `cycle_hash`, `provenance`.
|
||||
|
||||
### 3) Advisories
|
||||
- `GET /ledger/export/advisories`
|
||||
- Filters: `shape`, `since_sequence/until_sequence`, `severity`, `source` (feed id), `cwe_id`, `kev` (bool), `cvss_version`, `cvss_score_min`, `cvss_score_max`.
|
||||
- Response item: `advisory_id`, `source`, `title`, `description`, `cwes`, `cvss` (version, vector, base_score), `published`, `modified`, `status`, `epss` (score, percentile), `projection_version`, `cycle_hash`, `provenance`.
|
||||
|
||||
### 4) SBOMs
|
||||
- `GET /ledger/export/sboms`
|
||||
- Filters: `shape`, `since_sequence/until_sequence`, `since_observed_at/until_observed_at`, `subject_digest` (OCI digest), `sbom_format` (`spdx-json` \| `cyclonedx-json`), `component_purl` (repeatable), `contains_native` (bool), `slsa_build_type` (string).
|
||||
- Response item: `sbom_id`, `subject` (digest, media_type), `sbom_format`, `created_at`, `components_count`, `has_vulnerabilities` (bool), `materials` (digests), `projection_version`, `cycle_hash`, `provenance`.
|
||||
|
||||
## Pagination
|
||||
- Cursor param: `page_token` (opaque base64url JSON: `{ "last": { "event_sequence": long, "projection_version": string, "cycle_hash": string }, "filters_hash": sha256 }`).
|
||||
- Page size: `page_size` (default 500, max 5000). Server rejects if `page_size` differs across pages for same token.
|
||||
- Response envelope (for both NDJSON and JSON array):
|
||||
- Header `X-Stella-Next-Page-Token` when more data exists.
|
||||
- Header `X-Stella-Result-Count` with items count.
|
||||
- When `Prefer: return=minimal` is set, omit envelope body for NDJSON; clients rely on headers.
|
||||
|
||||
## Error Contract
|
||||
- 400: unknown filter, invalid range, or filter combination mismatch with `filters_hash` inside `page_token`.
|
||||
- 401/403: missing or insufficient scope.
|
||||
- 409: requested `shape` not compatible with downstream air-gap mode (compact requested where policy mandates canonical).
|
||||
- 429: enforcement of determinism guard (server detected projection drift vs cycle_hash); include `X-Stella-Drift-Reason`.
|
||||
|
||||
## Observability & Determinism Hooks
|
||||
- Emit structured logs `ledger.export.request` and `ledger.export.emit` with tenant, endpoint, filters_hash, page_size, result_count, duration_ms.
|
||||
- Counters: `ledger_export_items_total{endpoint,tenant}` and `ledger_export_failures_total{endpoint,reason}`.
|
||||
- Traces: span name `ledger.export.{endpoint}`; attach `filters_hash`, `page_size`, `next_page_token_present` attributes.
|
||||
|
||||
## artefact location
|
||||
- This document: `docs/modules/findings-ledger/export-http-surface.md` (hash stability: keep deterministic ordering as authored on 2025-11-20).
|
||||
- Link from sprint 0121 PREP-LEDGER-EXPORT-35-001; downstream tasks should reference this path for contract details.
|
||||
28
docs/modules/findings-ledger/oas-baseline.md
Normal file
28
docs/modules/findings-ledger/oas-baseline.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Findings Ledger OAS baseline (v1)
|
||||
|
||||
**Scope.** Establish the canonical OpenAPI baseline and API host definitions for Findings Ledger. This satisfies PREP-LEDGER-OAS-61-001 and unblocks downstream OAS/SDK/OBS tasks (61-002..63-001).
|
||||
|
||||
**What shipped (2025-11-20).**
|
||||
- Published baseline OAS document: `docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml` (OpenAPI 3.0.3).
|
||||
- Servers: `https://{env}.ledger.api.stellaops.local` (env ∈ dev/staging/prod/airgap) and offline `https://ledger.{region}.offline.bundle`.
|
||||
- Security: `bearerAuth` (JWT) and `mTLS` declared; tenant header `X-Stella-Tenant` required.
|
||||
- Paths included:
|
||||
- `GET /v1/ledger/events` (deterministic paging by chain/sequence; cursor header).
|
||||
- `POST /v1/ledger/events` (append; validates hashes; 409 on non-deterministic input).
|
||||
- `GET /v1/ledger/projections/findings` (projection read with `cycleHash`).
|
||||
- Schemas align with `docs/modules/findings-ledger/schema.md` (ledger events, projections, hashes, provenance fields).
|
||||
|
||||
**Usage / next steps.**
|
||||
- Export to renderer or client generation from `findings-ledger.v1.yaml`; keep schema source of truth in YAML, not code-first.
|
||||
- Downstream tasks (61-002, 62-001, 63-001) should extend this spec with SDK/validation/deprecation headers but **must not** change base paths, security schemes, or canonical field names.
|
||||
- When new fields are added, update both `schema.md` and the YAML and bump `info.version` with changelog entry.
|
||||
|
||||
**Determinism & offline posture.**
|
||||
- Stable ordering: events sorted by `(chainId, sequence)`; projections do not perform consensus/merges.
|
||||
- No external calls required at runtime; offline host is declared for bundle deployments.
|
||||
- Hash fields (`eventHash`, `previousHash`, `merkleLeafHash`, `cycleHash`) remain lowercase hex SHA-256 per schema.
|
||||
|
||||
**Artifact locations.**
|
||||
- OAS YAML: `docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml`
|
||||
- Schema reference: `docs/modules/findings-ledger/schema.md`
|
||||
- Export surface reference (for later OAS extensions): `docs/modules/findings-ledger/export-http-surface.md`
|
||||
264
docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml
Normal file
264
docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml
Normal file
@@ -0,0 +1,264 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: StellaOps Findings Ledger API
|
||||
version: 1.0.0-beta1
|
||||
description: >-
|
||||
Canonical, aggregation-only surface for append-only findings events, projections, and
|
||||
Merkle anchoring metadata. Aligns with schema in docs/modules/findings-ledger/schema.md.
|
||||
servers:
|
||||
- url: https://{env}.ledger.api.stellaops.local
|
||||
description: Default environment-scoped host
|
||||
variables:
|
||||
env:
|
||||
default: prod
|
||||
enum: [dev, staging, prod, airgap]
|
||||
- url: https://ledger.{region}.offline.bundle
|
||||
description: Offline bundle host for air-gapped deployments
|
||||
variables:
|
||||
region:
|
||||
default: local
|
||||
enum: [local]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- mTLS: []
|
||||
paths:
|
||||
/v1/ledger/events:
|
||||
get:
|
||||
summary: List ledger events
|
||||
operationId: listLedgerEvents
|
||||
tags: [ledger]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- name: chainId
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: sinceSequence
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 200
|
||||
maximum: 1000
|
||||
responses:
|
||||
'200':
|
||||
description: Paged ledger events in deterministic order (chainId, sequence_No asc)
|
||||
headers:
|
||||
Req-Cursor:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LedgerEventPage'
|
||||
post:
|
||||
summary: Append deterministic ledger event
|
||||
operationId: appendLedgerEvent
|
||||
tags: [ledger]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LedgerEventAppendRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Event persisted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LedgerEvent'
|
||||
'409':
|
||||
description: Hash/sequence conflict (non-deterministic input)
|
||||
/v1/ledger/projections/findings:
|
||||
get:
|
||||
summary: Get latest projection for findings
|
||||
operationId: listFindingProjections
|
||||
tags: [projections]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- name: findingId
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: policyVersion
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: status
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 200
|
||||
maximum: 1000
|
||||
responses:
|
||||
'200':
|
||||
description: Projection rows with cycleHash for replay validation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindingProjectionPage'
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
mTLS:
|
||||
type: mutualTLS
|
||||
parameters:
|
||||
TenantId:
|
||||
name: X-Stella-Tenant
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
schemas:
|
||||
LedgerEvent:
|
||||
type: object
|
||||
required: [event]
|
||||
properties:
|
||||
event:
|
||||
type: object
|
||||
required: [id, type, tenant, chainId, sequence, policyVersion, occurredAt, recordedAt, payload]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
type:
|
||||
type: string
|
||||
description: ledger_event_type (see schema.md §2.2)
|
||||
tenant:
|
||||
type: string
|
||||
chainId:
|
||||
type: string
|
||||
format: uuid
|
||||
sequence:
|
||||
type: integer
|
||||
policyVersion:
|
||||
type: string
|
||||
finding:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
artifactId: { type: string }
|
||||
vulnId: { type: string }
|
||||
actor:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
type: { type: string }
|
||||
occurredAt:
|
||||
type: string
|
||||
format: date-time
|
||||
recordedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
payload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
evidenceBundleRef:
|
||||
type: string
|
||||
eventHash:
|
||||
type: string
|
||||
previousHash:
|
||||
type: string
|
||||
merkleLeafHash:
|
||||
type: string
|
||||
LedgerEventPage:
|
||||
type: object
|
||||
required: [items]
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LedgerEvent'
|
||||
nextCursor:
|
||||
type: string
|
||||
LedgerEventAppendRequest:
|
||||
type: object
|
||||
required: [id, type, tenant, chainId, sequence, policyVersion, occurredAt, payload]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
type:
|
||||
type: string
|
||||
tenant:
|
||||
type: string
|
||||
chainId:
|
||||
type: string
|
||||
format: uuid
|
||||
sequence:
|
||||
type: integer
|
||||
policyVersion:
|
||||
type: string
|
||||
finding:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
artifactId: { type: string }
|
||||
vulnId: { type: string }
|
||||
actor:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
type: { type: string }
|
||||
occurredAt:
|
||||
type: string
|
||||
format: date-time
|
||||
payload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
evidenceBundleRef:
|
||||
type: string
|
||||
previousHash:
|
||||
type: string
|
||||
description: Optional; validated if supplied
|
||||
merkleLeafHash:
|
||||
type: string
|
||||
description: Optional; server recomputes to validate determinism
|
||||
FindingProjection:
|
||||
type: object
|
||||
required: [tenantId, findingId, policyVersion, status, cycleHash]
|
||||
properties:
|
||||
tenantId: { type: string }
|
||||
findingId: { type: string }
|
||||
policyVersion: { type: string }
|
||||
status: { type: string }
|
||||
severity:
|
||||
type: number
|
||||
format: double
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
currentEventId:
|
||||
type: string
|
||||
format: uuid
|
||||
cycleHash:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
FindingProjectionPage:
|
||||
type: object
|
||||
required: [items]
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FindingProjection'
|
||||
nextCursor:
|
||||
type: string
|
||||
@@ -0,0 +1,19 @@
|
||||
# Ledger OAS Prep — PREP-LEDGER-OAS-61-001/61-002/62-001/63-001
|
||||
|
||||
Status: Draft (2025-11-20)
|
||||
Owners: Findings Ledger Guild · API Contracts Guild · SDK Generator Guild
|
||||
Scope: Capture OAS baseline, HTTP surface, SDK generation, and deprecation flow for ledger service.
|
||||
|
||||
## What’s needed
|
||||
- Baseline OAS for ledger API host (61-001).
|
||||
- Confirm endpoints and auth scopes to publish (61-002 depends on 61-001).
|
||||
- SDK generation targets and language list (62-001).
|
||||
- Deprecation header plan once SDK validated (63-001).
|
||||
|
||||
## Open decisions
|
||||
- Hostname/versioning scheme for ledger API.
|
||||
- Auth scopes per endpoint.
|
||||
- Languages for SDK generation and package naming.
|
||||
|
||||
## Handoff
|
||||
Use this prep doc for LEDGER-OAS-61-001/61-002/62-001/63-001; update once API contract is drafted.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Ledger Observability Prep — PREP-LEDGER-OBS-54-001
|
||||
|
||||
Status: Draft (2025-11-20)
|
||||
Owners: Findings Ledger Guild · Provenance Guild
|
||||
Scope: Minimal API surface for `/ledger/attestations` and observability hooks.
|
||||
|
||||
## Needs
|
||||
- HTTP surface spec (routes, auth scopes) to host `/ledger/attestations`.
|
||||
- Telemetry fields to include provenance IDs.
|
||||
|
||||
## Handoff
|
||||
Use as PREP artefact; update once API contract is drafted.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Ledger Packs Snapshot Prep — PREP-LEDGER-PACKS-42-001
|
||||
|
||||
Status: Draft (2025-11-20)
|
||||
Owners: Findings Ledger Guild · Mirror Creator Guild
|
||||
Scope: Snapshot/time-travel contract for packs simulation.
|
||||
|
||||
## Needs
|
||||
- Snapshot format and bundle layout for pack simulation/time-travel.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Ledger Risk Schema Prep — PREP-LEDGER-RISK-66-001/002
|
||||
|
||||
Status: Draft (2025-11-20)
|
||||
Owners: Findings Ledger Guild · Risk Engine Guild
|
||||
|
||||
## Needs
|
||||
- Risk engine schema/contract inputs: `risk_score`, `risk_severity`, `profile_version`, `explanation_id`, indexes.
|
||||
- Migration plan to add fields.
|
||||
|
||||
## Handoff
|
||||
Use as PREP artefact; update when risk field definitions and rollout plan are available.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Ledger Risk Prep — PREP-LEDGER-RISK-68-001 / 69-001 / TEN-48-001
|
||||
|
||||
Status: Draft (2025-11-20)
|
||||
Owners: Findings Ledger Guild · Export Guild · Observability Guild · Platform
|
||||
|
||||
## 68-001 (scored findings export) needs
|
||||
- Inputs from 67-001 + Export Center contract for scored findings.
|
||||
- Metrics dimensions alignment with Observability.
|
||||
|
||||
## 69-001 (metrics) needs
|
||||
- Final metrics fields driven by 67-001/68-001 outputs.
|
||||
|
||||
## TEN-48-001 (RLS/partitioning) needs
|
||||
- Platform-approved tenant/project partitioning and RLS policy.
|
||||
|
||||
## Handoff
|
||||
Use as PREP artefact for the above tasks; update when upstream exports and RLS decisions are available.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Ledger attestation HTTP surface (prep for LEDGER-OBS-54-001 / 55-001)
|
||||
|
||||
**Goal.** Provide the minimal HTTP contract to expose ledger attestation verifications so PREP-LEDGER-OBS-55-001 can proceed. This complements the OAS baseline (`docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml`) and schema (`docs/modules/findings-ledger/schema.md`).
|
||||
|
||||
## Endpoint
|
||||
- `GET /v1/ledger/attestations`
|
||||
- Tenant header: `X-Stella-Tenant` (required).
|
||||
- Auth: bearer `scope=ledger.attest.read` or mTLS.
|
||||
- Query params:
|
||||
- `artifactId` (string, optional; OCI digest or SBOM id)
|
||||
- `findingId` (string, optional)
|
||||
- `attestationId` (uuid, optional)
|
||||
- `status` (`verified|failed|unknown`, optional)
|
||||
- `sinceRecordedAt` / `untilRecordedAt` (ISO-8601 UTC)
|
||||
- `limit` (int, default 200, max 1000)
|
||||
- Ordering: deterministic by `recordedAt ASC, attestationId ASC`.
|
||||
- Response: JSON array (or NDJSON when `Accept: application/x-ndjson`). Each item:
|
||||
- `attestationId` (uuid)
|
||||
- `artifactId` (string)
|
||||
- `findingId` (string)
|
||||
- `verificationStatus` (`verified|failed|unknown`)
|
||||
- `verificationTime` (ISO-8601 UTC)
|
||||
- `dsseDigest` (sha256)
|
||||
- `rekorEntryId` (string, optional)
|
||||
- `evidenceBundleRef` (string, optional)
|
||||
- `ledgerEventId` (uuid) — source ledger event that linked the attestation
|
||||
- `recordedAt` (ISO-8601 UTC)
|
||||
- `merkleLeafHash` (sha256)
|
||||
- `rootHash` (sha256)
|
||||
|
||||
## Determinism/offline posture
|
||||
- Sorting keys are fixed; pagination token encodes `{recordedAt, attestationId, filtersHash}`.
|
||||
- No live Rekor calls; `rekorEntryId` is stored reference only.
|
||||
- Hashes remain lowercase SHA-256; times are UTC.
|
||||
|
||||
## Artefact location
|
||||
- This prep doc: `docs/modules/findings-ledger/prep/ledger-attestations-http.md`.
|
||||
- Add path to OAS in a follow-on increment (LEDGER-OAS-61-002/63-001) once approved.
|
||||
Reference in New Issue
Block a user