365Architect

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.json document is auto-generated via Microsoft.AspNetCore.OpenApi and 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

BASH
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:

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)

BASH
# 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:

JSON
{
  "clientId": "admin-001",
  "secret": "any"
}

Success (200):

JSON
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Failure (401):

JSON
{
  "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):

JSON
{
  "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):

JSON
{
  "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):

JSON
{
  "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):

JSON
{
  "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):

JSON
{
  "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):

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "project": "/path/to/repo"
}

Roles: ConsultantAdmin

Response (200):

JSON
{
  "id": "a1b2c3d4e5f6",
  "status": "completed",
  "findings": 42
}

Response (500 — scan failed):

JSON
{
  "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):

JSON
[
  {
    "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):

JSON
[
  {
    "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:

JSON
{
  "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

BASH
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:

BASH
# 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)

CSHARP
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

  1. Create a controller class in Controllers/V1/ annotated with [ApiController] and [Route("v1")].
  2. Add [Authorize] to the controller or action.
  3. Register the endpoint in RoleAuthorization.EndpointPermissions with the allowed roles.
  4. Rebuild — OpenAPI picks up the new action automatically.
CSHARP
[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

  1. Add the enum value to ApiRole in Models/ApiCredential.cs.
  2. Seed the identity in LocalIdentityProvider constructor.
  3. Add the role to EndpointPermissions for each endpoint that should be accessible.
  4. Update the portal's PortalUserState with any new capability checks.

Replacing the Local IdP

The LocalIdentityProvider and JwtTokenService are registered as singletons. To replace:

  1. Implement a class that exposes Authenticate(clientId, secret) -> string? and ValidateToken(token) -> UserIdentity?.
  2. Register it in Program.cs instead of LocalIdentityProvider.
  3. Update JwtBearerOptions.TokenValidationParameters to match your IdP's signing key, issuer, and audience.

Replacing the Knowledge Base

Swap InMemoryKnowledgeBase for a signed-file KB:

CSHARP
// In Program.cs:
var kb = SignedKnowledgeBase.Load("kb-v2.signed");
builder.Services.AddSingleton<IKnowledgeBase>(kb);

Testing with curl

BASH
# 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

POWERSHELL
$body = @{ clientId = "admin-001"; secret = "any" } | ConvertTo-Json
tokenResp=InvokeRestMethodUri"http://localhost:5000/v1/auth/token"MethodPostBodytokenResp = Invoke-RestMethod -Uri "http://localhost:5000/v1/auth/token" -Method Post -Bodybody -ContentType "application/json"
headers = @{ Authorization = "Bearer($tokenResp.token)" }
findings=InvokeRestMethodUri"http://localhost:5000/v1/findings?project=."Headersfindings = Invoke-RestMethod -Uri "http://localhost:5000/v1/findings?project=." -Headersheaders
Write-Host "Findings: ((findings.count) (Broken: ((findings.quantumBroken))"

Python — Submit Questionnaire

PYTHON
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

CSHARP
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
Share

Keyboard Shortcuts

⌘ K
Open search
/
Focus search
?
Show shortcuts
b
Toggle bookmark
Alt+←
Previous page
Alt+→
Next page
Esc
Close overlay