Skip to content

Commit 5601263

Browse files
maxaloneclaude
andcommitted
feat: collettore di evidenze settimanale (§6.3) + Integrità parziale
generators/collect-evidence.mjs: interroga /api/status, /api/status-history, /api/health-log (ultimi 7 giorni), /ping, issue status-alert e commit recenti dei repo pubblici (solo GET, letture pubbliche); scrive uno snapshot in snapshots/YYYY-Www/ con manifest.json (SHA-256 di ogni file) e aggiorna last_seen sulle 7 evidenze corrispondenti nel registro (sostituzione mirata sul testo grezzo, non un dump YAML che ne stravolgerebbe la formattazione). Nuovo workflow collect-evidence.yml: schedulato ogni lunedì + workflow_dispatch, commit automatico se cambia qualcosa. score.mjs legge ora l'ultimo snapshot per il componente "worker" (sonda HMAC) di /api/status: l'indicatore Integrità passa da sempre n/d a parzialmente calcolabile (100/50/0 per ok/degraded/down), ma resta esplicitamente marcato come parziale — sia nel JSON (note) sia nel Trust Center (asterisco + tooltip) — perché non include ancora storico release taggate né esito ancore OTS mensili. Mai far passare un valore incompleto per completo. Primo snapshot reale (2026-W28) incluso: score 90->94, 5->6 indicatori disponibili su 10. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent df30742 commit 5601263

28 files changed

Lines changed: 2970 additions & 34 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Collect evidence snapshot
2+
3+
on:
4+
schedule:
5+
- cron: "0 6 * * 1" # ogni lunedì alle 06:00 UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
concurrency:
12+
group: collect-evidence
13+
cancel-in-progress: false
14+
15+
jobs:
16+
collect:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: 22
23+
- run: npm ci
24+
- run: npm run collect-evidence
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
- name: Commit lo snapshot e l'aggiornamento di last_seen, se cambia qualcosa
28+
run: |
29+
git config user.name "github-actions[bot]"
30+
git config user.email "github-actions[bot]@users.noreply.github.com"
31+
git add snapshots/ registry/evidence/
32+
git diff --cached --quiet || git commit -m "chore: snapshot evidenze $(date -u +%F)"
33+
git push

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
- "schemas/**"
99
- "generators/**"
1010
- "site/**"
11+
- "snapshots/**"
1112
- "package.json"
1213
- ".github/workflows/publish.yml"
1314
workflow_dispatch:

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,29 @@ servizio, è il progetto del sistema che la produce. Il Trust Center pubblico
2020
implementazioni, evidenze, processi, decisioni, rischi, dati, metriche).
2121
Uno schema JSON per tipo in `schemas/`.
2222
- `generators/` — script Node: `validate.mjs` (schema + integrità del grafo),
23-
`score.mjs` (Open Trust Score dal solo registro), `build-site.mjs`
24-
(genera `site/index.html`).
23+
`collect-evidence.mjs` (snapshot settimanale da endpoint pubblici),
24+
`score.mjs` (Open Trust Score dal registro + ultimo snapshot),
25+
`build-site.mjs` (genera `site/index.html`).
2526
- `site/` — Trust Center pubblico, generato — non modificarlo a mano.
26-
- `snapshots/` — bundle di evidenze raccolte nel tempo (non ancora popolato,
27-
vedi ADR-GTF-004).
27+
- `snapshots/` — bundle di evidenze raccolte settimanalmente dal collettore
28+
(vedi ADR-GTF-004 per la collocazione nel repo invece che su R2).
2829

2930
## Sviluppo locale
3031

3132
```bash
3233
npm install
33-
npm run validate # schema, integrità dei riferimenti, anti-segreti
34-
npm run build # validate + score + build-site (come in CI)
34+
npm run validate # schema, integrità dei riferimenti, anti-segreti
35+
npm run collect-evidence # snapshot da endpoint pubblici (sola lettura)
36+
npm run build # validate + score + build-site (come in CI)
3537
```
3638

3739
## Stato
3840

39-
**M3 v0** completata: Trust Center generato dal registro (missione, principi,
40-
posizionamento eIDAS, Compliance Map, rischi, decisioni) + primo calcolo
41-
dell'Open Trust Score (5 dei 10 indicatori calcolabili solo dal registro, gli
42-
altri dichiaratamente `n/d`). Prossimi passi in
43-
[ARCHITECTURE.md §12](./ARCHITECTURE.md#12-roadmap--wbs).
41+
**M4 in corso**: canary HMAC (P17-B) attivo e verificato in produzione;
42+
collettore di evidenze settimanale attivo (sblocca parzialmente l'indicatore
43+
Integrità dello score). Restano operative: ancoraggio dogfooding mensile,
44+
restore drill, review esterna annuale, pubblicazione di `autart-signer`
45+
(P11). Dettagli in [ARCHITECTURE.md §12](./ARCHITECTURE.md#12-roadmap--wbs).
4446

4547
## Licenza
4648

generators/build-site.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ function phaseNumber(id) {
4141
}
4242

4343
function renderLedger(score) {
44+
const hasPartial = score.indicators.some((i) => i.value !== null && i.note);
4445
const rows = score.indicators
4546
.map((i) => {
4647
if (i.value === null) {
4748
return ` <tr class="is-pending" title="${esc(i.note)}"><td class="ledger-label">${esc(i.label)}</td><td class="ledger-value">n/d</td></tr>`;
4849
}
50+
if (i.note) {
51+
return ` <tr class="is-counted is-partial" title="${esc(i.note)}"><td class="ledger-label">${esc(i.label)} *</td><td class="ledger-value">${i.value}</td></tr>`;
52+
}
4953
return ` <tr class="is-counted"><td class="ledger-label">${esc(i.label)}</td><td class="ledger-value">${i.value}</td></tr>`;
5054
})
5155
.join("\n");
@@ -56,7 +60,7 @@ ${rows}
5660
<tr class="ledger-total"><td class="ledger-label">Saldo</td><td class="ledger-value">${score.overall}<span class="unit">/100</span></td></tr>
5761
</tbody>
5862
</table>
59-
<p class="ledger-note">${score.available_count} di ${score.total} indicatori disponibili — i restanti non sono stimati: restano <em>n/d</em> finché non esisteranno i dati per calcolarli davvero. <a href="https://github.com/SPAZIO-GENESI/gtf/tree/main/registry/metrics">Formula di ciascuno</a>.</p>
63+
<p class="ledger-note">${score.available_count} di ${score.total} indicatori disponibili — i restanti non sono stimati: restano <em>n/d</em> finché non esisteranno i dati per calcolarli davvero.${hasPartial ? " * = valore parziale, passa il mouse per i dettagli." : ""} <a href="https://github.com/SPAZIO-GENESI/gtf/tree/main/registry/metrics">Formula di ciascuno</a>.</p>
6064
</div>`;
6165
}
6266

@@ -253,6 +257,8 @@ const STYLE = `
253257
.ledger-value { text-align: right; font-family: var(--font-mono); }
254258
tr.is-pending .ledger-label, tr.is-pending .ledger-value { color: var(--ink-muted); font-style: italic; cursor: help; }
255259
tr.is-pending .ledger-value::before { content: "— "; }
260+
tr.is-partial { cursor: help; }
261+
tr.is-partial .ledger-label { border-bottom: 1px dotted var(--ink-muted); }
256262
tr.ledger-total td { padding-top: 0.6rem; border-top: 3px double var(--ink); font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; }
257263
.ledger-total .unit { font-size: 1rem; font-weight: 400; color: var(--ink-muted); }
258264
.ledger-note { font-size: 0.85rem; color: var(--ink-muted); margin: 0.8rem 0 0; }

generators/collect-evidence.mjs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
2+
import { createHash } from "node:crypto";
3+
import { join } from "node:path";
4+
import { ROOT } from "./lib/registry.mjs";
5+
6+
const SNAPSHOTS_DIR = join(ROOT, "snapshots");
7+
const REGISTRY_EVIDENCE_DIR = join(ROOT, "registry", "evidence");
8+
9+
function isoWeek(date) {
10+
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
11+
const dayNum = (d.getUTCDay() + 6) % 7;
12+
d.setUTCDate(d.getUTCDate() - dayNum + 3);
13+
const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
14+
const week = 1 + Math.round(((d - firstThursday) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7);
15+
return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
16+
}
17+
18+
async function fetchJson(url, headers = {}) {
19+
try {
20+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(15000) });
21+
if (!res.ok) return { ok: false, status: res.status, url };
22+
return { ok: true, data: await res.json(), url };
23+
} catch (e) {
24+
return { ok: false, error: e.message, url };
25+
}
26+
}
27+
28+
function sha256(text) {
29+
return createHash("sha256").update(text).digest("hex");
30+
}
31+
32+
// Aggiorna last_seen con una sostituzione mirata sul testo grezzo, non un
33+
// dump YAML completo: preserva formattazione, commenti e ordine dei campi
34+
// del file esistente (un dump js-yaml li riscriverebbe tutti).
35+
function updateLastSeen(evdId, dateStr) {
36+
const file = join(REGISTRY_EVIDENCE_DIR, `${evdId}.yaml`);
37+
if (!existsSync(file)) return false;
38+
let text = readFileSync(file, "utf8");
39+
if (/^last_seen:/m.test(text)) {
40+
text = text.replace(/^last_seen:.*$/m, `last_seen: ${dateStr}`);
41+
} else if (/^collection:.*$/m.test(text)) {
42+
text = text.replace(/^(collection:.*)$/m, `$1\nlast_seen: ${dateStr}`);
43+
} else {
44+
return false;
45+
}
46+
writeFileSync(file, text);
47+
return true;
48+
}
49+
50+
async function main() {
51+
const ghHeaders = process.env.GITHUB_TOKEN
52+
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Accept: "application/vnd.github+json" }
53+
: { Accept: "application/vnd.github+json" };
54+
55+
const today = new Date();
56+
const week = isoWeek(today);
57+
const dir = join(SNAPSHOTS_DIR, week);
58+
mkdirSync(dir, { recursive: true });
59+
60+
const results = {};
61+
const evdHits = new Set();
62+
63+
results.status = await fetchJson("https://imgauth.spaziogenesi.org/api/status");
64+
if (results.status.ok) evdHits.add("EVD-status-live");
65+
66+
results["status-history"] = await fetchJson("https://imgauth.spaziogenesi.org/api/status-history");
67+
if (results["status-history"].ok) evdHits.add("EVD-r2-status-history");
68+
69+
const healthLog = {};
70+
for (let i = 0; i < 7; i++) {
71+
const d = new Date(today);
72+
d.setUTCDate(d.getUTCDate() - i);
73+
const day = d.toISOString().slice(0, 10);
74+
healthLog[day] = await fetchJson(`https://imgauth.spaziogenesi.org/api/health-log?day=${day}`);
75+
}
76+
results["health-log"] = healthLog;
77+
if (Object.values(healthLog).some((r) => r.ok)) evdHits.add("EVD-d1-health-log");
78+
79+
results["ping-imgauth"] = await fetchJson("https://imgauth.spaziogenesi.org/ping");
80+
if (results["ping-imgauth"].ok) evdHits.add("EVD-versions-live");
81+
82+
results["monitor-issues"] = await fetchJson(
83+
"https://api.github.com/repos/SPAZIO-GENESI/imgauth/issues?labels=status-alert&state=all&per_page=20",
84+
ghHeaders
85+
);
86+
if (results["monitor-issues"].ok) evdHits.add("EVD-monitor-issues");
87+
88+
results["git-imgauth"] = await fetchJson("https://api.github.com/repos/SPAZIO-GENESI/imgauth/commits?per_page=5", ghHeaders);
89+
if (results["git-imgauth"].ok) evdHits.add("EVD-git-imgauth");
90+
91+
results["git-imgauthweb"] = await fetchJson("https://api.github.com/repos/SPAZIO-GENESI/imgauthweb/commits?per_page=5", ghHeaders);
92+
if (results["git-imgauthweb"].ok) evdHits.add("EVD-git-imgauthweb");
93+
94+
const manifest = { collected_at: today.toISOString(), week, files: {} };
95+
for (const [name, data] of Object.entries(results)) {
96+
const filename = `${name}.json`;
97+
const text = JSON.stringify(data, null, 2) + "\n";
98+
writeFileSync(join(dir, filename), text);
99+
manifest.files[filename] = sha256(text);
100+
}
101+
writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
102+
103+
const todayStr = today.toISOString().slice(0, 10);
104+
let updated = 0;
105+
for (const id of evdHits) {
106+
if (updateLastSeen(id, todayStr)) updated++;
107+
}
108+
109+
console.log(`Snapshot ${week} scritto in ${dir}: ${Object.keys(results).length} file, ${updated} evidenze con last_seen aggiornato.`);
110+
}
111+
112+
main();

generators/score.mjs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
1-
import { writeFileSync } from "node:fs";
1+
import { writeFileSync, readdirSync, readFileSync, existsSync } from "node:fs";
22
import { join } from "node:path";
33
import { ROOT, loadRegistry, byFolder } from "./lib/registry.mjs";
44

55
const SITE_DIR = join(ROOT, "site");
6+
const SNAPSHOTS_DIR = join(ROOT, "snapshots");
7+
8+
// Legge l'ultimo snapshot settimanale del collettore (generators/collect-evidence.mjs),
9+
// se esiste, per il solo componente "worker" (sonda HMAC) di /api/status.
10+
// Le cartelle YYYY-Www ordinano correttamente in lessicografico (settimana a 2 cifre).
11+
function latestSnapshotWorkerStatus() {
12+
if (!existsSync(SNAPSHOTS_DIR)) return null;
13+
const weeks = readdirSync(SNAPSHOTS_DIR, { withFileTypes: true })
14+
.filter((d) => d.isDirectory())
15+
.map((d) => d.name)
16+
.sort();
17+
if (weeks.length === 0) return null;
18+
const latest = weeks[weeks.length - 1];
19+
const file = join(SNAPSHOTS_DIR, latest, "status.json");
20+
if (!existsSync(file)) return null;
21+
try {
22+
const wrapper = JSON.parse(readFileSync(file, "utf8"));
23+
if (!wrapper.ok || !wrapper.data?.worker) return null;
24+
return { week: latest, worker: wrapper.data.worker };
25+
} catch {
26+
return null;
27+
}
28+
}
629

730
function isPublic(record) {
831
return (record.visibility ?? "public") === "public";
@@ -50,9 +73,18 @@ function computeIndicators(records) {
5073
// Riproducibilità: stesso segnale del secondo termine di Trasparenza (limite dichiarato in MET-reproducibility)
5174
const reproducibility = pctCtlVerify;
5275

76+
// Integrità: parziale, solo componente "worker" (sonda HMAC) dell'ultimo snapshot
77+
// raccolto — non include ancora storico release taggate né esito ancore OTS mensili.
78+
const snap = latestSnapshotWorkerStatus();
79+
const WORKER_SCORE = { ok: 100, degraded: 50, down: 0 };
80+
const integrity = snap ? WORKER_SCORE[snap.worker] ?? null : null;
81+
const integrityNote = snap
82+
? `parziale: solo sonda HMAC (componente "worker", snapshot ${snap.week}) — non ancora storico release taggate né esito ancore OTS mensili`
83+
: "richiede almeno uno snapshot dal collettore di evidenze (non ancora raccolto)";
84+
5385
return [
5486
{ id: "MET-transparency", label: "Trasparenza", value: transparency },
55-
{ id: "MET-integrity", label: "Integrità", value: null, note: "richiede il collettore di evidenze live (non ancora costruito)" },
87+
{ id: "MET-integrity", label: "Integrità", value: integrity, note: integrityNote },
5688
{ id: "MET-traceability", label: "Tracciabilità", value: traceability },
5789
{ id: "MET-documentation", label: "Documentazione", value: documentation },
5890
{ id: "MET-automation", label: "Automazione", value: automation },

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"description": "Genesis Trust Framework — registro, schemi e generatori del Trust Center di Spazio Genesi ETS",
77
"scripts": {
88
"validate": "node generators/validate.mjs",
9+
"collect-evidence": "node generators/collect-evidence.mjs",
910
"score": "node generators/score.mjs",
1011
"build-site": "node generators/build-site.mjs",
1112
"build": "npm run validate && npm run score && npm run build-site"

registry/evidence/EVD-d1-health-log.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: Log fine degli eventi di salute (errori, degradi, rallentamenti)
33
location: { system: cloudflare-d1, ref: "imgauth-health/health_log", public_via: "GET /api/health-log?day=" }
44
produced_by: PRC-status-sampling
55
collection: auto
6+
last_seen: 2026-07-09
67
freshness_max_days: 1
78
integrity: "append-only per convenzione; snapshot mensile hashato+ancorato (EVD-monthly-anchor)"
89
supports: [CTL-availability-monitoring]

registry/evidence/EVD-git-imgauth.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ id: EVD-git-imgauth
22
title: Storia git del motore di attestazione (repo pubblico imgauth, licenza AGPL-3.0)
33
location: { system: github, ref: "repo imgauth, storia commit + tag di versione" }
44
collection: auto
5+
last_seen: 2026-07-09
56
integrity: "storia immutabile di fatto (force-push escluso dalle convenzioni operative); ogni bump di versione tracciato in package.json"
67
visibility: public

registry/evidence/EVD-git-imgauthweb.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ id: EVD-git-imgauthweb
22
title: Storia git dell'interfaccia utente (repo pubblico imgauthweb, licenza MIT)
33
location: { system: github, ref: "repo imgauthweb, storia commit + tag di versione" }
44
collection: auto
5+
last_seen: 2026-07-09
56
integrity: "storia immutabile di fatto; ogni bump di APP_VERSION tracciato in index.html"
67
visibility: public

0 commit comments

Comments
 (0)