CipherShift365 — REST API /v1
Component: CipherShift365.Portal.Api
Base path: /v1
Protocol: HTTP/1.1 over JSON
Auth scheme: Bearer JWT (local IdP)
Target framework: .NET 10.0 (ASP.NET Core minimal hosting model)
What It Does
The REST API is the single source of truth for the CipherShift365 portal tier. Every piece of data displayed in the Blazor Portal, every scan triggered, every PDF rendered, and every questionnaire submitted flows through this API. It exposes a versioned /v1 prefix that covers the full engagement lifecycle: cryptographic asset discovery (CBOM), findings, risk scores, CNSA 2.0 compliance verdicts, prioritized migration plans, branded PDF generation, scan history, audit trails, and the readiness-questionnaire engine.
Key design decisions:
- No external identity provider. Authentication is handled by a built-in local IdP issuing JWT tokens. The system boots with three pre-seeded identities covering the full RBAC matrix.
- No database. All scan results, history, and audit entries are held in-memory via
ConcurrentBag<T>collections. The API is stateless after restart. - No internet dependency. The API runs entirely on-premises, air-gapped, with zero outbound calls.
- OpenAPI. The
/openapi/v1.jsondocument is auto-generated viaMicrosoft.AspNetCore.OpenApiand served from the same process.
Why It's Needed (Value Proposition)
| Need | How the REST API addresses it |
|---|---|
| Air-gapped operations | Local JWT IdP — no Azure AD, no OIDC provider, no internet. The token endpoint runs inside the same boundary. |
| Role-based access for regulated estates | Three roles (Executive, ConsultantAdmin, Auditor) with granular endpoint permissions enforced per-request. |
| Programmatic integration | Any CI/CD agent, automation script, or custom dashboard can call the API with curl/PowerShell/Python. |
| Single engagement tier | The portal, PDF generator, and questionnaire all read from the same API — no data duplication. |
| Audit compliance | Every mutating request (POST/PUT/PATCH/DELETE) is recorded with actor, action, target, and timestamp. |
Installation
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 10.0 |
| OS | Windows 10+ / Windows Server 2019+ / Linux x64 |
| RAM | 512 MB (API process) |
| Network | None required |
| External services | None required |
Build & Run
cd src/Portal/CipherShift365.Portal.Api
dotnet restore
dotnet run --urls http://localhost:5000
The API starts on http://localhost:5000 (override with --urls). In production, place a reverse proxy (nginx, IIS ARR, YARP) in front — the API itself listens on HTTP only.
Configuration
All configuration lives in appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
There are no database connection strings, no external service URLs, no secret store references. The JWT signing key is hard-coded for the engagement tier (rotate in production).
How to Run / Use
Quick Start (2 steps)
# Step 1 — Obtain a JWT token
curl -s -X POST http://localhost:5000/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"clientId":"admin-001","secret":"any"}'
# Response: { "token": "eyJhbGciOiJIUzI1NiIs..." }
# Step 2 — Use the token
curl -s http://localhost:5000/v1/findings?project=. \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Token Lifetime
Tokens expire 8 hours after issuance. The clock skew tolerance is zero — expired tokens are rejected immediately.
Pre-Seeded Identities
| Client ID | Display Name | Role | Typical use |
|---|---|---|---|
exec-001 |
CEO | Executive |
View-only: dashboards, findings, reports |
admin-001 |
Security Admin | ConsultantAdmin |
Full access: scan, manage, render, audit |
auditor-001 |
Compliance Auditor | Auditor |
Review findings, CNSA verdict, questionnaire |
User Manual
Authentication Flow
Client API
| |
|-- POST /v1/auth/token ------->|
| { clientId, secret } |
| |-- Lookup in LocalIdentityProvider
| |-- Issue JWT (8h expiry, HMAC-SHA256)
|<---- { token } ---------------|
| |
|-- GET /v1/findings?project=. ->|
| Authorization: Bearer <tok> |
| |-- Validate JWT (iss, aud, sig, exp)
| |-- Extract role claim
| |-- Authorize against EndpointPermissions
| |-- If authorized: run scan, return JSON
|<---- { findings: [...] } -----|
JWT Claims
| Claim | Value |
|---|---|
sub (NameIdentifier) |
exec-001 / admin-001 / auditor-001 |
unique_name (Name) |
CEO / Security Admin / Compliance Auditor |
role |
Executive / ConsultantAdmin / Auditor |
iss |
ciphershift365.local |
aud |
ciphershift365.api |
exp |
UTC now + 8 hours |
RBAC Matrix
| Endpoint | Method | Executive | ConsultantAdmin | Auditor |
|---|---|---|---|---|
/v1/auth/token |
POST | — (anon) | — (anon) | — (anon) |
/v1/cbom |
GET | Read | Read | Read |
/v1/findings |
GET | Read | Read | Read |
/v1/scores |
GET | Read | Read | Read |
/v1/cnsa-verdict |
GET | Read | Read | Read |
/v1/plan |
GET | Read | Read | Read |
/v1/reports |
GET | Read | Read | — |
/v1/reports/render |
POST | — | Execute | — |
/v1/scans |
POST | — | Execute | — |
/v1/history |
GET | Read | Read | Read |
/v1/audit |
GET | — | Read | — |
/v1/questionnaire/templates |
GET | — | Read | Read |
/v1/questionnaire/templates/{id} |
GET | — | Read | Read |
/v1/questionnaire/submit |
POST | — | Execute | Execute |
/v1/questionnaire/responses |
GET | — | Read | Read |
/v1/questionnaire/responses/{id} |
GET | — | Read | Read |
Authorization is enforced in Auth/RoleAuthorization.cs. Every [Authorize] controller validates the caller's role claim against the matrix. Unauthorized requests receive 403 Forbidden.
API Reference
1. Authentication
POST /v1/auth/token
Obtain a JWT bearer token.
Request body:
{
"clientId": "admin-001",
"secret": "any"
}
Success (200):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Failure (401):
{
"error": "Invalid credentials."
}
Notes: The secret field is accepted but not validated in the default local IdP. The identity is resolved purely by clientId. This is intentional for the air-gapped engagement tier — replace LocalIdentityProvider with an actual credential store for production.
2. Cryptographic Bill of Materials
`GET /v1/cbom?project=
Returns the signed CBOM (Cryptographic Bill of Materials) as JSON.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
project |
string | Yes | — | File-system path to scan (. for CWD) |
Roles: Executive, ConsultantAdmin, Auditor
Response (200):
{
"header": {
"version": "1.0.0",
"projectPath": ".",
"kbVersion": "1.0.0",
"generatedAt": "2026-06-29T12:00:00Z"
},
"assets": [
{
"logicalId": "abc123",
"locationalId": "abc123::Program.cs:42",
"role": "Signature",
"algorithm": "RSA",
"parameters": { "keySize": 2048 },
"riskLevel": "QuantumBroken"
}
],
"signature": "base64-signature"
}
Error (500):
{
"title": "Scan incomplete",
"status": 500
}
3. Findings
`GET /v1/findings?project=
Returns all cryptographic findings from a scan, grouped by risk category.
Query parameters:
| Param | Type | Required | Default |
|---|---|---|---|
project |
string | Yes | — |
Roles: Executive, ConsultantAdmin, Auditor
Response (200):
{
"count": 42,
"quantumBroken": 18,
"quantumVulnerable": 12,
"mitigable": 8,
"quantumSafe": 4,
"findings": [
{
"algorithm": "RSA",
"role": "Signature",
"risk": "QuantumBroken",
"confidence": "High",
"location": "C:\\src\\Program.cs",
"line": 42,
"description": "RSA-2048 is vulnerable to Shor's algorithm."
}
]
}
4. Risk Scores
`GET /v1/scores?project=
Returns the system-level risk score and component count.
Response (200):
{
"systemScore": 0.72,
"componentCount": 12,
"findingCount": 42
}
| Field | Range | Interpretation |
|---|---|---|
systemScore |
0.0 – 1.0 | 0 = all quantum-safe, 1 = all quantum-broken |
componentCount |
integer | Distinct algorithm types detected |
findingCount |
integer | Total individual crypto asset detections |
5. CNSA 2.0 Compliance
`GET /v1/cnsa-verdict?project=
Validates the scanned CBOM against the CNSA 2.0 profile and returns a compliance verdict with gap analysis.
Roles: Executive, ConsultantAdmin, Auditor
Response (200):
{
"verdict": "NonCompliant",
"compliant": 4,
"nonCompliant": 14,
"gaps": [
{
"algorithm": "RSA",
"gapDescription": "RSA is not an approved CNSA 2.0 signature algorithm.",
"recommendedAction": "Migrate to ML-DSA-65 (FIPS 204) or LMS/XMSS for code-signing."
},
{
"algorithm": "ECDH",
"gapDescription": "ECDH is not an approved CNSA 2.0 key establishment algorithm.",
"recommendedAction": "Migrate to ML-KEM-768 (FIPS 203) or ECDH-NIST with PQC hybrid."
}
],
"evidence": {
"profileName": "CNSA 2.0",
"profileVersion": "1.0.0"
}
}
Verdict values: Compliant, NonCompliant, Indeterminate
6. Migration Plan
`GET /v1/plan?project=
Returns a prioritized migration plan with urgency, target algorithm, effort estimates, and rationale.
Roles: Executive, ConsultantAdmin, Auditor
Response (200):
{
"totalEffortDays": 64,
"itemCount": 18,
"items": [
{
"urgency": "Critical",
"algorithm": "RSA",
"target": "ML-DSA-65",
"effortDays": 3,
"rationale": "RSA-2048 in key signing path — quantum-broken, must migrate before 2030."
}
]
}
Urgency levels: Critical, High, Medium, Low
7. Reports
`GET /v1/reports?project=
Generates and returns a human-readable report.
| Param | Values | Default |
|---|---|---|
format |
html, md |
html |
Roles: Executive, ConsultantAdmin
Response (200 — HTML): Content-Type: text/html — full HTML report with embedded CSS.
Response (200 — Markdown): Content-Type: text/markdown — Markdown suitable for rendering or check-in.
POST /v1/reports/render
Generates a branded PDF using QuestPDF.
Request body:
{
"project": ".",
"organization": "Acme Corp"
}
| Field | Type | Required | Default |
|---|---|---|---|
project |
string | Yes | — |
organization |
string | No | "365Architect" |
Roles: ConsultantAdmin
Response (200): Content-Type: application/pdf with Content-Disposition: attachment filename ciphershift-report-{date}.pdf.
8. Scan Trigger
POST /v1/scans
Triggers a cryptographic scan, records history, and writes an audit entry.
Request body:
{
"project": "/path/to/repo"
}
Roles: ConsultantAdmin
Response (200):
{
"id": "a1b2c3d4e5f6",
"status": "completed",
"findings": 42
}
Response (500 — scan failed):
{
"id": "a1b2c3d4e5f6",
"status": "failed",
"findings": 0
}
9. Scan History
GET /v1/history
Returns all prior scan records, ordered by timestamp descending.
Roles: Executive, ConsultantAdmin, Auditor
Response (200):
[
{
"id": "a1b2c3d4e5f6",
"path": ".",
"findingCount": 42,
"score": 0.72,
"status": "completed",
"timestamp": "2026-06-29T12:00:00Z"
}
]
10. Audit Trail
GET /v1/audit
Returns the full audit log of mutating operations.
Roles: ConsultantAdmin
Response (200):
[
{
"actor": "Security Admin",
"action": "POST",
"target": "/v1/scans",
"timestamp": "2026-06-29T12:00:00Z",
"detail": "Status: 200"
}
]
All POST, PUT, PATCH, and DELETE requests are automatically recorded by AuditMiddleware. The actor is extracted from the JWT unique_name claim. Unauthenticated requests are recorded as "anonymous".
11. Questionnaire
See the full Questionnaire Engine documentation.
| Endpoint | Method | Description |
|---|---|---|
/v1/questionnaire/templates |
GET | List all assessment templates |
/v1/questionnaire/templates/{id} |
GET | Get template with full question tree |
/v1/questionnaire/submit |
POST | Submit an assessment response |
/v1/questionnaire/responses |
GET | List all responses with scores |
/v1/questionnaire/responses/{id} |
GET | Get a specific response with detailed score |
Error Handling
Error Response Format
All errors are returned in a consistent JSON envelope:
{
"error": "Internal server error",
"id": "a1b2c3d4"
}
The id field is a truncated GUID (8 hex chars) that can be correlated with server logs for diagnostics. Internal exception details are never exposed to the client.
HTTP Status Codes
| Status | Meaning |
|---|---|
200 OK |
Success |
400 Bad Request |
Invalid input (missing required fields) |
401 Unauthorized |
Missing or invalid JWT |
403 Forbidden |
Role not authorized for this endpoint |
404 Not Found |
Resource not found (unknown template ID, response ID) |
500 Internal Server Error |
Unhandled exception (masked with error ID) |
The ErrorHandlingMiddleware catches all unhandled exceptions, masks them, and returns a sanitized 500 response.
OpenAPI / Swagger
The OpenAPI document is auto-generated from controller metadata and served at /openapi/v1.json.
Accessing the Spec
curl http://localhost:5000/openapi/v1.json
Using with Swagger UI
Since the API is air-gapped, Swagger UI is not bundled. Use a local copy:
# Option A — npx serve + remote Swagger UI
npx swagger-ui-watcher openapi/v1.json --port 5500
# Option B — Use VS Code REST Client
# Option C — Download the spec and import into Postman/Insomnia
OpenAPI Registration (Program.cs)
builder.Services.AddOpenApi(); // registers the document generator
app.MapOpenApi(); // serves /openapi/v1.json
Air-Gapped Operation
The API is designed to run with zero network connectivity:
| Concern | Solution |
|---|---|
| Identity Provider | Built-in LocalIdentityProvider with pre-seeded identities — no external IdP call |
| JWT Signing | HMAC-SHA256 symmetric key — no JWKS endpoint, no certificate store |
| Knowledge Base | In-memory InMemoryKnowledgeBase seeded at startup — no database, no file system dependency for KB |
| Audit Storage | In-memory ConcurrentBag<AuditEntry> — portable across restarts (loses state; add persistent store for production) |
| CORS | AllowAnyOrigin() — suitable for same-machine or trusted-network deployment only |
Developer Manual
Project Structure
CipherShift365.Portal.Api/
├── Program.cs # Host builder, DI registration, middleware pipeline
├── Auth/
│ ├── JwtTokenService.cs # JWT generation + validation (HMAC-SHA256)
│ ├── LocalIdentityProvider.cs # Pre-seeded identity store
│ └── RoleAuthorization.cs # RBAC endpoint matrix
├── Controllers/V1/
│ ├── AuthController.cs # POST /v1/auth/token
│ ├── ReadControllers.cs # CBOM, Findings, Scores, CNSA (4 controllers)
│ ├── ActionControllers.cs # Plan, Reports, Scan, History/Audit (5 controllers)
│ └── QuestionnaireController.cs # Questionnaire CRUD
├── Models/
│ └── ApiCredential.cs # UserIdentity, ApiRole enum
├── Services/
│ └── ScanService.cs # Scan orchestration + ScanHistoryService + AuditService
├── Middleware/
│ └── AuditMiddleware.cs # Audit logging + ErrorHandlingMiddleware
├── Properties/
│ └── launchSettings.json
├── appsettings.json
└── CipherShift365.Portal.Api.csproj
Adding a New Endpoint
- Create a controller class in
Controllers/V1/annotated with[ApiController]and[Route("v1")]. - Add
[Authorize]to the controller or action. - Register the endpoint in
RoleAuthorization.EndpointPermissionswith the allowed roles. - Rebuild — OpenAPI picks up the new action automatically.
[ApiController]
[Route("v1")]
public class CustomController : ControllerBase
{
[HttpGet("custom")]
[Authorize]
public IActionResult Get() => Ok(new { message = "Hello" });
}
// In RoleAuthorization.cs:
["GET:/v1/custom"] = new[] { ApiRole.Executive, ApiRole.ConsultantAdmin },
Adding a New Role
- Add the enum value to
ApiRoleinModels/ApiCredential.cs. - Seed the identity in
LocalIdentityProviderconstructor. - Add the role to
EndpointPermissionsfor each endpoint that should be accessible. - Update the portal's
PortalUserStatewith any new capability checks.
Replacing the Local IdP
The LocalIdentityProvider and JwtTokenService are registered as singletons. To replace:
- Implement a class that exposes
Authenticate(clientId, secret) -> string?andValidateToken(token) -> UserIdentity?. - Register it in
Program.csinstead ofLocalIdentityProvider. - Update
JwtBearerOptions.TokenValidationParametersto match your IdP's signing key, issuer, and audience.
Replacing the Knowledge Base
Swap InMemoryKnowledgeBase for a signed-file KB:
// In Program.cs:
var kb = SignedKnowledgeBase.Load("kb-v2.signed");
builder.Services.AddSingleton<IKnowledgeBase>(kb);
Testing with curl
# Full integration test
TOKEN=$(curl -s -X POST http://localhost:5000/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"clientId":"admin-001","secret":"any"}' | jq -r .token)
echo "--- FINDINGS ---"
curl -s http://localhost:5000/v1/findings?project=. \
-H "Authorization: Bearer $TOKEN" | jq .
echo "--- SCORES ---"
curl -s http://localhost:5000/v1/scores?project=. \
-H "Authorization: Bearer $TOKEN" | jq .
echo "--- CNSA ---"
curl -s http://localhost:5000/v1/cnsa-verdict?project=. \
-H "Authorization: Bearer $TOKEN" | jq .
echo "--- PLAN ---"
curl -s http://localhost:5000/v1/plan?project=. \
-H "Authorization: Bearer $TOKEN" | jq .
echo "--- AUDIT ---"
curl -s http://localhost:5000/v1/audit \
-H "Authorization: Bearer $TOKEN" | jq .
Code Examples
PowerShell — Authenticate and Fetch Findings
$body = @{ clientId = "admin-001"; secret = "any" } | ConvertTo-Json
body -ContentType "application/json"
headers = @{ Authorization = "Bearer($tokenResp.token)" }
headers
Write-Host "Findings: findings.count) (Broken: findings.quantumBroken))"
Python — Submit Questionnaire
import requests
BASE = "http://localhost:5000"
token = requests.post(f"{BASE}/v1/auth/token", json={"clientId": "admin-001", "secret": "any"}).json()["token"]
headers = {"Authorization": f"Bearer {token}"}
resp = requests.post(f"{BASE}/v1/questionnaire/submit", headers=headers, json={
"templateId": "pqc-readiness-v1",
"answers": [
{"questionId": "q1.1", "value": "Yes, fully applicable", "score": 15},
{"questionId": "q1.2", "value": "CNSA 2.0 compliance mandate", "score": 10},
]
})
print(resp.json()["score"]["readinessPercent"])
C# HttpClient — Render PDF
var client = new HttpClient { BaseAddress = new Uri("http://localhost:5000") };
// Authenticate
var authResp = await client.PostAsJsonAsync("/v1/auth/token", new { clientId = "admin-001", secret = "any" });
var authJson = await authResp.Content.ReadFromJsonAsync<JsonElement>();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authJson.GetProperty("token").GetString());
// Render PDF
var pdfResp = await client.PostAsJsonAsync("/v1/reports/render", new { project = ".", organization = "Acme Corp" });
var pdf = await pdfResp.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("report.pdf", pdf);
Configuration Options
| Setting | File | Default | Description |
|---|---|---|---|
Logging:LogLevel:Default |
appsettings.json | Information |
Log verbosity |
Logging:LogLevel:Microsoft.AspNetCore |
appsettings.json | Warning |
Framework log verbosity |
AllowedHosts |
appsettings.json | * |
Host header filtering |
ApiBaseUrl |
Portal appsettings | http://localhost:5000 |
Portal's API target (not consumed by API itself) |
| JWT issuer | JwtTokenService ctor |
ciphershift365.local |
iss claim value |
| JWT audience | JwtTokenService ctor |
ciphershift365.api |
aud claim value |
| JWT signing key | JwtTokenService ctor |
Hard-coded 45-char string | Rotate via code change |
| JWT expiry | JwtTokenService |
8 hours | Token lifetime |
| JWT clock skew | JwtBearerOptions |
TimeSpan.Zero |
No drift tolerance |
| KB entries | Program.cs |
4 entries (RSA, ECDSA, ECDH, AES) | Seed InMemoryKnowledgeBase |
| KB version | Program.cs |
1.0.0 |
Available through IKnowledgeBase.Version |
Third-Party Dependencies
| Package | Version | License | Use |
|---|---|---|---|
Microsoft.AspNetCore.Authentication.JwtBearer |
10.0.0 | MIT | JWT validation middleware |
Microsoft.AspNetCore.OpenApi |
10.0.0 | MIT | OpenAPI document generation |
CipherShift365.Core.Analysis |
(project ref) | Proprietary | Analysis engine, CNSA, assessment |
CipherShift365.Compass |
(project ref) | Proprietary | Discovery pipeline, report generators |
CipherShift365.Portal.Pdf |
(project ref) | Proprietary | Branded PDF generation |