feat(dsms): Stufe 1 — Gap-Analyse Report wird in DSMS archiviert
Build + Deploy / build-admin-compliance (push) Successful in 1m41s
Build + Deploy / build-backend-compliance (push) Successful in 14s
Build + Deploy / build-ai-sdk (push) Successful in 41s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 10s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m31s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 48s
CI / test-python-backend (push) Failing after 1s
CI / test-python-document-crawler (push) Successful in 32s
CI / test-python-dsms-gateway (push) Successful in 25s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m23s
Build + Deploy / build-admin-compliance (push) Successful in 1m41s
Build + Deploy / build-backend-compliance (push) Successful in 14s
Build + Deploy / build-ai-sdk (push) Successful in 41s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 10s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m31s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 48s
CI / test-python-backend (push) Failing after 1s
CI / test-python-document-crawler (push) Successful in 32s
CI / test-python-dsms-gateway (push) Successful in 25s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m23s
- Go DSMS Client (internal/dsms/client.go): Archive() + Verify() - Python DSMS Client (compliance/services/dsms_client.py): archive_to_dsms() + verify_dsms() - Gap-Analyse AnalyzeProject() archiviert Report-JSON nach DSMS - Response enthält dsms_cid wenn Archivierung erfolgreich - Frontend: Grünes "Revisionssicher archiviert" Badge mit CID im GapDashboard - DSMS Proxy Route (/api/sdk/v1/dsms/[...path]) für Verify-Abfragen Stufe 2 (Evidence Upload → DSMS) und Stufe 3 (Version Chains) folgen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* DSMS Gateway Proxy — forwards verify/history requests to dsms-gateway.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const DSMS_URL = process.env.DSMS_GATEWAY_URL || 'http://dsms-gateway:8082'
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path } = await params
|
||||
const target = `${DSMS_URL}/api/v1/${path.join('/')}`
|
||||
|
||||
try {
|
||||
const resp = await fetch(target, {
|
||||
headers: { Authorization: 'Bearer system-frontend' },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
})
|
||||
const data = await resp.json()
|
||||
return NextResponse.json(data, { status: resp.status })
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'DSMS not available' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface GapReport {
|
||||
dsms_cid?: string
|
||||
profile_name: string
|
||||
regulations: Array<{
|
||||
id: string
|
||||
@@ -79,6 +80,20 @@ export function GapDashboard({ report, onBack }: Props) {
|
||||
← Neue Analyse
|
||||
</button>
|
||||
|
||||
{/* DSMS Archive Badge */}
|
||||
{report.dsms_cid && (
|
||||
<div className="mb-4 flex items-center gap-2 px-4 py-2.5 bg-emerald-50 border border-emerald-200 rounded-lg">
|
||||
<svg className="w-4 h-4 text-emerald-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<span className="text-sm text-emerald-800 font-medium">Revisionssicher archiviert</span>
|
||||
<code className="text-xs text-emerald-600 bg-emerald-100 px-2 py-0.5 rounded font-mono">
|
||||
{report.dsms_cid.length > 20 ? report.dsms_cid.slice(0, 8) + '...' + report.dsms_cid.slice(-6) : report.dsms_cid}
|
||||
</code>
|
||||
<span className="text-[10px] text-emerald-500">DSMS/IPFS</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<SummaryCard
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/dsms"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/gap"
|
||||
)
|
||||
|
||||
@@ -104,7 +108,27 @@ func (h *GapHandler) AnalyzeProject(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, report)
|
||||
// Archive gap report to DSMS (non-blocking, best-effort)
|
||||
var dsmsCID string
|
||||
reportJSON, _ := json.Marshal(report)
|
||||
filename := fmt.Sprintf("gap-report-%s-%s.json", id.String()[:8], time.Now().Format("2006-01-02"))
|
||||
if result := dsms.Archive(reportJSON, filename, "gap_report", id.String(), "1"); result != nil {
|
||||
dsmsCID = result.CID
|
||||
}
|
||||
|
||||
// Return report with DSMS CID appended
|
||||
response := gin.H{
|
||||
"profile_id": report.ProfileID,
|
||||
"profile_name": report.ProfileName,
|
||||
"regulations": report.Regulations,
|
||||
"summary": report.Summary,
|
||||
"gaps": report.Gaps,
|
||||
"created_at": report.CreatedAt,
|
||||
}
|
||||
if dsmsCID != "" {
|
||||
response["dsms_cid"] = dsmsCID
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// QuickAnalyze runs gap analysis without saving a project.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Package dsms provides a client for archiving compliance artifacts to the
|
||||
// DSMS gateway (IPFS-backed tamper-proof document storage).
|
||||
package dsms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ArchiveResult is the response from a successful DSMS archive operation.
|
||||
type ArchiveResult struct {
|
||||
CID string `json:"cid"`
|
||||
Size int `json:"size"`
|
||||
GatewayURL string `json:"gateway_url"`
|
||||
}
|
||||
|
||||
var gatewayURL = getEnv("DSMS_GATEWAY_URL", "http://dsms-gateway:8082")
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// Archive stores content in DSMS and returns the CID.
|
||||
// Non-critical: returns nil on failure (logs warning).
|
||||
func Archive(content []byte, filename, documentType, documentID, version string) *ArchiveResult {
|
||||
var buf bytes.Buffer
|
||||
w := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := w.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
log.Printf("[dsms] multipart error: %v", err)
|
||||
return nil
|
||||
}
|
||||
if _, err := part.Write(content); err != nil {
|
||||
log.Printf("[dsms] write error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = w.WriteField("document_type", documentType)
|
||||
_ = w.WriteField("document_id", documentID)
|
||||
_ = w.WriteField("version", version)
|
||||
_ = w.WriteField("language", "de")
|
||||
w.Close()
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
req, err := http.NewRequest("POST", gatewayURL+"/api/v1/documents", &buf)
|
||||
if err != nil {
|
||||
log.Printf("[dsms] request error: %v", err)
|
||||
return nil
|
||||
}
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
req.Header.Set("Authorization", "Bearer system-backend")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[dsms] unavailable: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != 200 {
|
||||
log.Printf("[dsms] archive failed (%d): %s", resp.StatusCode, string(body[:min(len(body), 200)]))
|
||||
return nil
|
||||
}
|
||||
|
||||
var result ArchiveResult
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
log.Printf("[dsms] parse error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[dsms] archived: %s → CID %s (%d bytes)", filename, result.CID, result.Size)
|
||||
return &result
|
||||
}
|
||||
|
||||
// Verify checks document integrity by CID.
|
||||
func Verify(cid string) (bool, error) {
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Get(fmt.Sprintf("%s/api/v1/verify/%s", gatewayURL, cid))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == 200, nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"""DSMS (Dezentrales Daten Speicher System) Client — archives compliance
|
||||
artifacts to IPFS via the dsms-gateway for tamper-proof versioning.
|
||||
|
||||
Usage:
|
||||
from compliance.services.dsms_client import archive_to_dsms, verify_dsms
|
||||
|
||||
result = await archive_to_dsms(
|
||||
content=pdf_bytes,
|
||||
filename="gap-report-2026-05-11.json",
|
||||
document_type="gap_report",
|
||||
document_id=str(report_id),
|
||||
version="1",
|
||||
)
|
||||
cid = result["cid"] # IPFS Content Identifier
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DSMS_GATEWAY_URL = os.environ.get("DSMS_GATEWAY_URL", "http://dsms-gateway:8082")
|
||||
|
||||
|
||||
async def archive_to_dsms(
|
||||
content: bytes,
|
||||
filename: str,
|
||||
document_type: str,
|
||||
document_id: str,
|
||||
version: str = "1",
|
||||
parent_cid: str | None = None,
|
||||
language: str = "de",
|
||||
tenant_id: str | None = None,
|
||||
) -> dict:
|
||||
"""Archive binary content to DSMS (IPFS).
|
||||
|
||||
Returns dict with keys: cid, size, gateway_url.
|
||||
Returns empty dict on failure (non-critical — logs warning).
|
||||
"""
|
||||
data = {
|
||||
"document_type": document_type,
|
||||
"document_id": document_id,
|
||||
"version": version,
|
||||
"language": language,
|
||||
}
|
||||
if parent_cid:
|
||||
data["parent_cid"] = parent_cid
|
||||
if tenant_id:
|
||||
data["tenant_id"] = tenant_id
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
resp = await client.post(
|
||||
f"{DSMS_GATEWAY_URL}/api/v1/documents",
|
||||
files={"file": (filename, content)},
|
||||
data=data,
|
||||
headers={"Authorization": "Bearer system-backend"},
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
logger.warning("DSMS archive failed (%d): %s", resp.status_code, resp.text[:200])
|
||||
return {}
|
||||
|
||||
result = resp.json()
|
||||
logger.info("DSMS archived: %s → CID %s (%d bytes)", filename, result.get("cid", "?"), result.get("size", 0))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("DSMS archive unavailable: %s", e)
|
||||
return {}
|
||||
|
||||
|
||||
async def verify_dsms(cid: str) -> dict:
|
||||
"""Verify a document's integrity by CID.
|
||||
|
||||
Returns dict with verification result or empty dict on failure.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(f"{DSMS_GATEWAY_URL}/api/v1/verify/{cid}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
return {"valid": False, "error": f"HTTP {resp.status_code}"}
|
||||
|
||||
return resp.json()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("DSMS verify unavailable: %s", e)
|
||||
return {"valid": False, "error": str(e)}
|
||||
Reference in New Issue
Block a user