diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8f93a6..722d27d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,20 +68,20 @@ jobs: npm run audit:ci -- \ --path ./examples/ci-safe-mcp.json \ --output ./.ci-mcp-inventory.json \ - --sarif ./.ci-secure-mpc.sarif + --sarif ./.ci-mcp-trustmap.sarif - name: Generate Kubernetes admission bundle run: | npm run generate:admission -- \ --policy-file ./examples/oci-policies.json \ - --namespace secure-mpc-ci \ + --namespace mcp-trustmap-ci \ --output ./.ci-kubernetes-admission - name: Validate Kubernetes admission bundle run: | npm run validate:admission -- \ --policy-file ./examples/oci-policies.json \ - --namespace secure-mpc-ci \ + --namespace mcp-trustmap-ci \ --bundle ./.ci-kubernetes-admission - name: Lint diff --git a/README.md b/README.md index 5bd020b..59b42a2 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,31 @@ -# Secure MPC +# MCP TrustMap [![CI](https://github.com/mawoole/SecureMPC/actions/workflows/ci.yml/badge.svg)](https://github.com/mawoole/SecureMPC/actions/workflows/ci.yml) -![Aperçu Secure MPC](public/og.png) +![Aperçu MCP TrustMap](public/og.png) -Secure MPC est une application web d’audit de configurations MCP +MCP TrustMap est une application web d’audit de configurations MCP ([Model Context Protocol](https://modelcontextprotocol.io/)). Elle transforme un inventaire de serveurs difficile à relire en une posture de sécurité claire : score global, risques prioritaires, explication de l’impact et correctifs directement applicables. +## Modules produit + +- **TrustMap Discover** construit la cartographie des serveurs, sources, + transports et composants découverts par le collecteur local ou par import. +- **TrustMap Audit** applique le référentiel de sécurité, priorise les écarts, + fournit les correctifs, gère les exceptions et conserve l’historique agrégé. +- **TrustMap CI** simule une politique sur l’inventaire courant et génère la + commande ainsi que le workflow GitHub Actions correspondant, avec SARIF, + CycloneDX, OSV et vérification de provenance configurables. +- **TrustMap Enterprise** mesure la couverture des propriétaires et des preuves, + présente la posture par équipe et exporte un pack de gouvernance JSON. + +Les vues Enterprise reflètent uniquement les données réellement chargées. +L’interface ne simule pas de SSO, de synchronisation multi-utilisateurs ni de +connexion à un annuaire d’entreprise. + > Le moteur actuel réalise une **analyse statique locale** des configurations. > Il ne remplace pas un test d’intrusion, une revue des permissions réellement > accordées ni une surveillance d’exécution. @@ -131,7 +147,7 @@ npm run build ## Exceptions de risque Une correction qui ne peut pas être appliquée immédiatement peut être placée -sous exception depuis le détail de l’écart. Secure MPC exige : +sous exception depuis le détail de l’écart. MCP TrustMap exige : - un motif explicite et, idéalement, une référence de suivi ; - un responsable identifié ; @@ -248,7 +264,7 @@ npm run collect -- --sbom ### Inventaire supply chain et SBOM -Secure MPC reconnaît les lanceurs suivants sans les exécuter : +MCP TrustMap reconnaît les lanceurs suivants sans les exécuter : - npm : `npx`, `npm exec`, `pnpm dlx`, `yarn dlx` et `bunx` ; - Python : `uvx` et `pipx run` ; @@ -317,7 +333,7 @@ et rattachés aux serveurs, directs comme transitifs : npm run collect -- --provenance ``` -Pour chaque composant, Secure MPC : +Pour chaque composant, MCP TrustMap : 1. exige que l’intégrité SRI du lockfile corresponde à `dist.integrity` ; 2. vérifie la signature ECDSA du registre sur @@ -415,7 +431,7 @@ la voie GitHub continue d’utiliser `gh`. Les références d’images peuvent être transmises au registre et au service de confiance choisi. Pour un registre privé, `cosign` ou `gh` peut réutiliser ses -propres identifiants déjà configurés ; Secure MPC ne lit ni ne conserve ces +propres identifiants déjà configurés ; MCP TrustMap ne lit ni ne conserve ces identifiants. ### Admission Kubernetes @@ -643,7 +659,7 @@ ou élevé est détecté : ```bash npm run audit:ci -- \ --path ./.mcp.json \ - --sarif ./secure-mpc.sarif + --sarif ./mcp-trustmap.sarif ``` Le seuil peut être adapté avec `--fail-on critical|high|medium`. Utilisez @@ -669,10 +685,10 @@ steps: - name: Audit MCP run: npm run audit:ci -- --path ./.mcp.json --sarif - name: Publier le rapport SARIF - if: always() && hashFiles('secure-mpc.sarif') != '' + if: always() && hashFiles('mcp-trustmap.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: - sarif_file: secure-mpc.sarif + sarif_file: mcp-trustmap.sarif ``` Les codes de sortie sont stables : `0` pour un contrôle réussi, `1` pour une diff --git a/app/globals.css b/app/globals.css index 9ef6a0b..89b2f17 100644 --- a/app/globals.css +++ b/app/globals.css @@ -39,7 +39,8 @@ body { button, input, -textarea { +textarea, +select { color: inherit; font: inherit; } @@ -1112,6 +1113,627 @@ button.table-row:hover { padding: 25px 0 0; } +.audit-subnav { + align-items: center; + border-bottom: 1px solid var(--line); + display: flex; + gap: 4px; + min-height: 54px; + overflow-x: auto; +} + +.audit-subnav button { + background: transparent; + border: 0; + border-bottom: 2px solid transparent; + color: var(--muted); + font-size: 11px; + font-weight: 650; + height: 54px; + padding: 0 14px; + white-space: nowrap; +} + +.audit-subnav button:hover, +.audit-subnav button.active { + color: var(--ink); +} + +.audit-subnav button.active { + border-bottom-color: var(--green); +} + +.module-view { + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 30px; +} + +.module-hero { + align-items: flex-end; + background: + radial-gradient(circle at 88% 12%, rgba(36, 87, 214, 0.12), transparent 30%), + linear-gradient(135deg, #15241f 0%, #1b3028 100%); + border-radius: 18px; + color: #fff; + display: flex; + gap: 32px; + justify-content: space-between; + min-height: 220px; + overflow: hidden; + padding: 34px 38px; + position: relative; +} + +.module-hero > div:first-child { + max-width: 690px; + position: relative; + z-index: 1; +} + +.module-hero h2 { + font-size: clamp(27px, 3.2vw, 45px); + letter-spacing: -0.055em; + line-height: 1.03; + margin: 12px 0 16px; +} + +.module-hero p { + color: rgba(255, 255, 255, 0.69); + font-size: 13px; + line-height: 1.65; + margin: 0; + max-width: 650px; +} + +.module-badge { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 99px; + color: #b9ead3; + display: inline-flex; + font-size: 9px; + font-weight: 760; + letter-spacing: 0.14em; + padding: 7px 10px; +} + +.module-actions { + display: flex; + flex: 0 0 auto; + gap: 9px; + position: relative; + z-index: 1; +} + +.module-hero .button.primary { + background: #f7fff9; + color: #173227; +} + +.module-hero .button.secondary { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); + color: #fff; +} + +.module-kpis, +.readiness-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.module-kpis article { + background: var(--paper); + border: 1px solid var(--line); + border-radius: 13px; + display: flex; + flex-direction: column; + min-height: 118px; + padding: 18px; +} + +.module-kpis span { + color: var(--muted); + font-size: 10px; + font-weight: 650; +} + +.module-kpis strong { + font-size: 30px; + letter-spacing: -0.05em; + margin: 12px 0 3px; +} + +.module-kpis small { + color: var(--subtle); + font-size: 9px; +} + +.module-grid, +.ci-layout, +.enterprise-layout { + display: grid; + gap: 16px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.module-card { + background: var(--paper); + border: 1px solid var(--line); + border-radius: 15px; + box-shadow: var(--shadow); + min-width: 0; + padding: 22px; +} + +.module-card-head { + align-items: flex-start; + display: flex; + gap: 16px; + justify-content: space-between; + margin-bottom: 20px; +} + +.module-card-head h3 { + font-size: 17px; + letter-spacing: -0.035em; + margin: 0; +} + +.module-card-head > small { + color: var(--subtle); + font-size: 9px; +} + +.distribution-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.distribution-list > div { + align-items: center; + display: grid; + gap: 12px; + grid-template-columns: minmax(105px, 1fr) minmax(80px, 2fr) 38px; +} + +.distribution-list span { + display: flex; + flex-direction: column; +} + +.distribution-list strong { + font-size: 11px; +} + +.distribution-list small, +.transport-grid small { + color: var(--subtle); + font-size: 9px; + margin-top: 2px; +} + +.distribution-list i { + background: var(--paper-soft); + border-radius: 99px; + height: 6px; + overflow: hidden; +} + +.distribution-list b { + background: var(--green); + border-radius: inherit; + display: block; + height: 100%; +} + +.distribution-list em { + color: var(--muted); + font-size: 10px; + font-style: normal; + text-align: right; +} + +.transport-grid { + display: grid; + gap: 10px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.transport-grid > div { + background: var(--ivory); + border: 1px solid var(--line); + border-radius: 11px; + display: grid; + grid-template-columns: 30px 1fr; + padding: 13px; +} + +.transport-grid > div > span { + align-items: center; + background: var(--green-soft); + border-radius: 8px; + color: var(--green-dark); + display: inline-flex; + grid-row: 1 / span 2; + height: 28px; + justify-content: center; + margin-right: 9px; + width: 28px; +} + +.transport-grid strong { + font-size: 11px; +} + +.inventory-list { + display: flex; + flex-direction: column; +} + +.inventory-list > button { + align-items: center; + background: transparent; + border: 0; + border-top: 1px solid var(--line); + display: grid; + gap: 14px; + grid-template-columns: 38px minmax(160px, 1fr) 90px 75px 16px; + min-height: 64px; + padding: 9px 4px; + text-align: left; +} + +.inventory-list > button:hover { + background: var(--ivory); +} + +.inventory-list > button > span:not(.server-icon, .inventory-status) { + display: flex; + flex-direction: column; +} + +.inventory-list strong { + font-size: 11px; +} + +.inventory-list small { + color: var(--subtle); + font-size: 9px; +} + +.inventory-list b { + color: var(--subtle); + font-weight: 500; +} + +.inventory-status { + border-radius: 99px; + font-size: 9px; + font-weight: 750; + padding: 5px 8px; + text-align: center; +} + +.inventory-status.secure { + background: var(--green-soft); + color: var(--green-dark); +} + +.inventory-status.attention { + background: var(--amber-soft); + color: var(--amber); +} + +.inventory-status.critical { + background: var(--coral-soft); + color: var(--coral); +} + +.gate-preview { + align-items: center; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 13px; + display: flex; + flex: 0 0 310px; + gap: 13px; + padding: 16px; + position: relative; + z-index: 1; +} + +.gate-preview > span { + align-items: center; + border-radius: 10px; + display: inline-flex; + flex: 0 0 auto; + font-size: 18px; + font-weight: 760; + height: 38px; + justify-content: center; + width: 38px; +} + +.gate-preview.passed > span { + background: rgba(64, 195, 133, 0.18); + color: #85e1b2; +} + +.gate-preview.blocked > span { + background: rgba(240, 96, 82, 0.18); + color: #ff9489; +} + +.gate-preview small { + color: rgba(255, 255, 255, 0.55); + display: block; + font-size: 8px; + letter-spacing: 0.1em; + margin-bottom: 4px; +} + +.gate-preview strong { + display: block; + font-size: 12px; +} + +.gate-preview p { + font-size: 9px; + line-height: 1.4; + margin-top: 4px; +} + +.policy-builder { + display: flex; + flex-direction: column; +} + +.policy-builder > label { + color: var(--muted); + display: flex; + flex-direction: column; + font-size: 10px; + font-weight: 650; + gap: 7px; + margin-bottom: 14px; +} + +.policy-builder input:not([type="checkbox"]), +.policy-builder select { + background: var(--ivory); + border: 1px solid var(--line-strong); + border-radius: 9px; + font-size: 11px; + min-height: 40px; + padding: 0 11px; +} + +.policy-checks { + border-top: 1px solid var(--line); + display: flex; + flex-direction: column; + margin-top: 3px; + padding-top: 8px; +} + +.policy-checks label { + align-items: center; + cursor: pointer; + display: flex; + gap: 10px; + min-height: 48px; +} + +.policy-checks input { + accent-color: var(--green); + height: 16px; + width: 16px; +} + +.policy-checks span { + display: flex; + flex-direction: column; +} + +.policy-checks strong { + font-size: 10px; +} + +.policy-checks small { + color: var(--subtle); + font-size: 8px; + margin-top: 2px; +} + +.workflow-card pre { + background: #111b18; + border-radius: 11px; + color: #dff0e7; + font-size: 9px; + line-height: 1.55; + margin: 0; + max-height: 500px; + overflow: auto; + padding: 17px; + white-space: pre; +} + +.command-card { + align-items: center; + background: #e9eefc; + border: 1px solid #cdd8f6; + border-radius: 13px; + display: flex; + gap: 20px; + justify-content: space-between; + padding: 17px 20px; +} + +.command-card code { + color: #1d3f9f; + display: block; + font-size: 10px; + overflow-wrap: anywhere; +} + +.readiness-score { + align-items: baseline; + display: flex; + flex: 0 0 auto; + position: relative; +} + +.readiness-score span { + color: rgba(255, 255, 255, 0.58); + font-size: 8px; + letter-spacing: 0.12em; + position: absolute; + right: 0; + top: -2px; + white-space: nowrap; +} + +.readiness-score strong { + font-size: 76px; + letter-spacing: -0.09em; + line-height: 1; +} + +.readiness-score small { + color: rgba(255, 255, 255, 0.55); + font-size: 13px; +} + +.readiness-grid article { + align-items: center; + background: var(--paper); + border: 1px solid var(--line); + border-radius: 13px; + display: flex; + gap: 12px; + min-height: 105px; + padding: 15px; +} + +.readiness-grid article > span { + align-items: center; + border-radius: 9px; + display: inline-flex; + flex: 0 0 auto; + font-weight: 750; + height: 32px; + justify-content: center; + width: 32px; +} + +.readiness-grid article.ready > span { + background: var(--green-soft); + color: var(--green-dark); +} + +.readiness-grid article.attention > span { + background: var(--coral-soft); + color: var(--coral); +} + +.readiness-grid small { + color: var(--muted); + display: block; + font-size: 9px; +} + +.readiness-grid strong { + display: block; + font-size: 20px; + margin: 3px 0; +} + +.readiness-grid p { + color: var(--subtle); + font-size: 8px; + margin: 0; +} + +.owner-table { + display: flex; + flex-direction: column; +} + +.owner-row { + align-items: center; + border-top: 1px solid var(--line); + display: grid; + font-size: 10px; + gap: 8px; + grid-template-columns: minmax(120px, 1fr) 65px 70px 65px; + min-height: 48px; +} + +.owner-row > span { + color: var(--muted); +} + +.owner-head { + border-top: 0; + color: var(--subtle); + font-size: 8px; + font-weight: 700; + min-height: 28px; + text-transform: uppercase; +} + +.danger-text { + color: var(--coral) !important; + font-weight: 750; +} + +.success-text { + color: var(--green) !important; + font-weight: 750; +} + +.governance-card { + display: flex; + flex-direction: column; +} + +.governance-stat { + align-items: center; + border-top: 1px solid var(--line); + display: flex; + justify-content: space-between; + min-height: 54px; +} + +.governance-stat span { + color: var(--muted); + font-size: 10px; +} + +.governance-stat strong { + font-size: 21px; +} + +.governance-card > p { + color: var(--muted); + font-size: 10px; + line-height: 1.55; +} + +.governance-card .button { + margin-top: auto; +} + +.enterprise-note { + color: var(--subtle); + font-size: 9px; + margin: -4px 3px 0; +} + .view-intro { align-items: flex-end; display: flex; @@ -2739,6 +3361,16 @@ button.table-row:hover { .history-layout { grid-template-columns: 1fr; } + + .module-hero { + align-items: flex-start; + flex-direction: column; + } + + .module-kpis, + .readiness-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 820px) { @@ -2826,6 +3458,25 @@ button.table-row:hover { flex-direction: column; gap: 15px; } + + .module-grid, + .ci-layout, + .enterprise-layout { + grid-template-columns: 1fr; + } + + .gate-preview { + flex-basis: auto; + width: 100%; + } + + .inventory-list > button { + grid-template-columns: 38px minmax(120px, 1fr) 70px 16px; + } + + .inventory-list > button > span:nth-child(3) { + display: none; + } } @media (max-width: 560px) { @@ -2969,6 +3620,60 @@ button.table-row:hover { .exception-form-grid { grid-template-columns: 1fr; } + + .module-view { + padding-top: 18px; + } + + .module-hero { + min-height: 0; + padding: 24px 21px; + } + + .module-hero h2 { + font-size: 29px; + } + + .module-actions, + .command-card { + align-items: stretch; + flex-direction: column; + width: 100%; + } + + .module-kpis, + .readiness-grid { + grid-template-columns: 1fr; + } + + .module-kpis article { + min-height: 94px; + } + + .transport-grid { + grid-template-columns: 1fr; + } + + .inventory-card { + padding: 16px; + } + + .inventory-list > button { + gap: 9px; + grid-template-columns: 34px minmax(110px, 1fr) 64px; + } + + .inventory-list > button > b { + display: none; + } + + .owner-row { + grid-template-columns: minmax(105px, 1fr) 55px 55px; + } + + .owner-row > span:nth-child(2) { + display: none; + } } @media (prefers-reduced-motion: reduce) { diff --git a/app/layout.tsx b/app/layout.tsx index 2a48e68..c19b317 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -20,9 +20,9 @@ export async function generateMetadata(): Promise { const protocol = forwardedProtocol ?? (host.startsWith("localhost") ? "http" : "https"); const baseUrl = new URL(`${protocol}://${host}`); - const title = "Secure MPC — Audit de sécurité MCP"; + const title = "MCP TrustMap — Cartographie, audit et gouvernance MCP"; const description = - "Analysez la configuration de vos serveurs MCP, priorisez les risques et appliquez des correctifs concrets."; + "Découvrez vos serveurs MCP, auditez leur sécurité, appliquez une politique CI et pilotez leur gouvernance."; return { metadataBase: baseUrl, @@ -37,7 +37,7 @@ export async function generateMetadata(): Promise { url: new URL("/og.png", baseUrl).toString(), width: 1732, height: 908, - alt: "Secure MPC — Vos serveurs MCP, sous contrôle.", + alt: "MCP TrustMap — Vos serveurs MCP, sous contrôle.", }, ], }, diff --git a/app/page.tsx b/app/page.tsx index 2d79e6b..431cd39 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -42,8 +42,12 @@ import { type AuditHistorySource, } from "../lib/audit-history"; import { AuditHistoryView } from "./audit-history-view"; +import { TrustMapDiscover } from "./trustmap-discover"; +import { TrustMapCi } from "./trustmap-ci"; +import { TrustMapEnterprise } from "./trustmap-enterprise"; -type View = "overview" | "servers" | "rules" | "history"; +type View = "discover" | "audit" | "ci" | "enterprise"; +type AuditView = "overview" | "servers" | "rules" | "history"; type ExceptionDraft = { findingKey: string; @@ -54,9 +58,11 @@ type ExceptionDraft = { maximumExpiresOn: string; }; -const RISK_EXCEPTIONS_STORAGE_KEY = "secure-mpc.risk-exceptions.v1"; -const LEGACY_RISK_EXCEPTIONS_STORAGE_KEY = - "mcp-sentinel.risk-exceptions.v1"; +const RISK_EXCEPTIONS_STORAGE_KEY = "mcp-trustmap.risk-exceptions.v1"; +const LEGACY_RISK_EXCEPTIONS_STORAGE_KEYS = [ + ["secure", "mpc.risk-exceptions.v1"].join("-"), + "mcp-sentinel.risk-exceptions.v1", +] as const; function dateInputValue(date: Date): string { const offset = date.getTimezoneOffset() * 60_000; @@ -380,7 +386,7 @@ const sampleConfig = `{ function BrandMark() { return ( ); } @@ -391,7 +397,8 @@ function StatusDot({ status }: { status: ServerStatus }) { export default function Home() { const [servers, setServers] = useState(demoServers); - const [view, setView] = useState("overview"); + const [view, setView] = useState("audit"); + const [auditView, setAuditView] = useState("overview"); const [filter, setFilter] = useState<"all" | ServerStatus>("all"); const [search, setSearch] = useState(""); const [selectedServer, setSelectedServer] = useState(null); @@ -417,9 +424,12 @@ export default function Home() { const savedExceptions = window.localStorage.getItem( RISK_EXCEPTIONS_STORAGE_KEY, ); - const legacyExceptions = window.localStorage.getItem( - LEGACY_RISK_EXCEPTIONS_STORAGE_KEY, + const legacyKey = LEGACY_RISK_EXCEPTIONS_STORAGE_KEYS.find((key) => + window.localStorage.getItem(key), ); + const legacyExceptions = legacyKey + ? window.localStorage.getItem(legacyKey) + : null; setRiskExceptions( parseRiskExceptions(savedExceptions ?? legacyExceptions), ); @@ -429,8 +439,8 @@ export default function Home() { RISK_EXCEPTIONS_STORAGE_KEY, legacyExceptions, ); - window.localStorage.removeItem( - LEGACY_RISK_EXCEPTIONS_STORAGE_KEY, + LEGACY_RISK_EXCEPTIONS_STORAGE_KEYS.forEach((key) => + window.localStorage.removeItem(key), ); } catch { // The persistence effect below will retry without blocking startup. @@ -848,8 +858,8 @@ export default function Home() { const date = generatedAt.toISOString().slice(0, 10); fileName = format === "cyclonedx" - ? `secure-mpc-${date}.cdx.json` - : `secure-mpc-${date}.${format}`; + ? `mcp-trustmap-${date}.cdx.json` + : `mcp-trustmap-${date}.${format}`; successMessage = format === "sarif" ? "Rapport SARIF exporté" @@ -877,25 +887,37 @@ export default function Home() { }; const navItems: { id: View; label: string; icon: string }[] = [ - { id: "overview", label: "Vue d’ensemble", icon: "⌂" }, - { id: "servers", label: "Serveurs", icon: "▦" }, - { id: "rules", label: "Règles de sécurité", icon: "✓" }, - { id: "history", label: "Historique", icon: "↗" }, + { id: "discover", label: "TrustMap Discover", icon: "◎" }, + { id: "audit", label: "TrustMap Audit", icon: "✓" }, + { id: "ci", label: "TrustMap CI", icon: "⌘" }, + { id: "enterprise", label: "TrustMap Enterprise", icon: "◇" }, ]; + const auditNavItems: { id: AuditView; label: string }[] = [ + { id: "overview", label: "Vue d’ensemble" }, + { id: "servers", label: "Serveurs" }, + { id: "rules", label: "Référentiel" }, + { id: "history", label: "Historique & exceptions" }, + ]; + + const notify = (message: string) => { + setToast(message); + window.setTimeout(() => setToast(""), 2600); + }; + return (