From 5f631e9b28d622d342db23b50ee59ef025292386 Mon Sep 17 00:00:00 2001 From: frank Date: Tue, 26 May 2026 19:31:59 +0200 Subject: [PATCH] refactor: restructure team logic and integrate teams.json configuration --- bitbucket-audit/index.js | 56 ----- bitbucket-audit/lib/config.js | 1 - bitbucket-audit/lib/reportTeam.js | 96 -------- bitbucket-audit/lib/teamConfig.js | 205 ------------------ bitbucket-audit/lib/teamOwnership.js | 139 ------------ frontend/app.js | 6 + frontend/js/data/report.js | 85 +------- frontend/js/data/teamData.js | 138 ++++++++++++ frontend/js/state.js | 2 + .../teams.example.json | 0 10 files changed, 152 insertions(+), 576 deletions(-) delete mode 100644 bitbucket-audit/lib/reportTeam.js delete mode 100644 bitbucket-audit/lib/teamConfig.js delete mode 100644 bitbucket-audit/lib/teamOwnership.js rename {bitbucket-audit => frontend}/teams.example.json (100%) diff --git a/bitbucket-audit/index.js b/bitbucket-audit/index.js index e0c863e..9191a49 100644 --- a/bitbucket-audit/index.js +++ b/bitbucket-audit/index.js @@ -10,9 +10,6 @@ const pooledMap = require("./lib/pool"); const { buildReport } = require("./lib/report"); const buildMarkdownReport = require("./lib/reportMarkdown"); const printReport = require("./lib/reportTerminal"); -const { loadTeamConfig, assignReposToTeams, buildTeamReport } = require("./lib/teamConfig"); -const { buildTeamMarkdownReport } = require("./lib/reportTeam"); -const { loadOwnership, buildLookupMap, enrichRepos, buildByTeam } = require("./lib/teamOwnership"); const secret = require("./secret"); // --------------------------------------------------------------------------- @@ -49,20 +46,6 @@ async function main() { const checks = require("./checks"); const checkIds = checks.map((c) => c.id).join(", "); - // Hent team-eierskap tidlig slik at status vises før repo-scanning starter - let ownershipJson = null; - try { - ownershipJson = await loadOwnership(config.TEAM_REPOS_MAPPING, request); - if (ownershipJson) { - const products = Object.keys(ownershipJson); - const teamEntries = products.flatMap((p) => Object.keys(ownershipJson[p]).map((t) => `${p}/${t}`)); - console.log(`Team-eierskap hentet (${products.length} produkt(er), ${teamEntries.length} team(er)): ${teamEntries.join(", ")}`); - } - } catch (err) { - console.error(`Advarsel: Kunne ikke laste team-eierskap — ${err.message}`); - console.error("Rapport skrives uten team-eierskap."); - } - // Hent prosjekter — ett spesifikt eller alle let projects; if (config.PROJECT_KEY) { @@ -197,16 +180,6 @@ async function main() { // Bygg og skriv rapport const report = buildReport(repoResults, checks); - // Berik med team-eierskap (data allerede hentet før scanning) - if (ownershipJson) { - const lookupMap = buildLookupMap(ownershipJson); - enrichRepos(report.repos, lookupMap); - report.summary.byTeam = buildByTeam(report.repos, checks, ownershipJson); - const assignedCount = report.repos.filter((r) => r.team !== null).length; - const unassignedCount = report.repos.length - assignedCount; - console.log(`\nTeam-tilknytning: ${assignedCount} repos tilknyttet, ${unassignedCount} uten team (unassigned)`); - } - const reportsDir = path.join(process.cwd(), "reports"); if (!fs.existsSync(reportsDir)) fs.mkdirSync(reportsDir, { recursive: true }); @@ -217,35 +190,6 @@ async function main() { fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf8"); fs.writeFileSync(mdPath, buildMarkdownReport(report, checks), "utf8"); - // Integrer team-data om teams.json finnes i prosjektets rotmappe - const teamConfigPath = path.join(__dirname, "..", "teams.json"); - if (fs.existsSync(teamConfigPath)) { - try { - const teamConfig = loadTeamConfig(teamConfigPath); - const teamAssignment = assignReposToTeams(report.repos, teamConfig); - report.teams = buildTeamReport(report.repos, teamConfig, checks); - report.teamAssignment = Object.fromEntries(teamAssignment); - - // Skriv oppdatert rapport-JSON med team-data - fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf8"); - - // Skriv én markdown-rapport per team - for (const team of report.teams) { - const teamRepos = report.repos.filter((r) => - (report.teamAssignment[`${r.project}/${r.repo}`] || "unassigned") === team.id - ); - const teamMd = buildTeamMarkdownReport(team, teamRepos, checks); - const teamMdPath = path.join(reportsDir, `team-${team.id}-${timestamp}.md`); - fs.writeFileSync(teamMdPath, teamMd, "utf8"); - } - - console.log(` Teams: ${report.teams.length} team (inkl. unassigned) lagt til rapporten.`); - } catch (err) { - console.error(`\nAdvarsel: Kunne ikke laste teams.json — ${err.message}`); - console.error("Rapport skrives uten team-data."); - } - } - console.log(`\nRapporter skrevet til:`); console.log(` JSON : ${jsonPath}`); console.log(` MD : ${mdPath}`); diff --git a/bitbucket-audit/lib/config.js b/bitbucket-audit/lib/config.js index 3ec8cc4..8f3c75c 100644 --- a/bitbucket-audit/lib/config.js +++ b/bitbucket-audit/lib/config.js @@ -25,7 +25,6 @@ const config = { MAX_REPOS: parseInt(process.env.MAX_REPOS, 10) || 0, // 0 = ingen grense PROJECT_KEY, OUTPUT_FORMAT, - TEAM_REPOS_MAPPING: process.env.TEAM_REPOS_MAPPING || null, }; function validateEnv() { diff --git a/bitbucket-audit/lib/reportTeam.js b/bitbucket-audit/lib/reportTeam.js deleted file mode 100644 index d2117b1..0000000 --- a/bitbucket-audit/lib/reportTeam.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict"; - -// --------------------------------------------------------------------------- -// Genererer en Markdown-rapport per team -// --------------------------------------------------------------------------- - -function buildTeamMarkdownReport(team, repoResults, checks) { - const date = new Date().toISOString().slice(0, 10); - const healthLabel = team.overallScore >= 80 ? "God" : team.overallScore >= 50 ? "Trenger tiltak" : "Kritisk"; - - let md = `# ${team.name} — Argus Teamrapport\n\n`; - md += `| | |\n|---|---|\n`; - md += `| **Dato** | ${date} |\n`; - md += `| **Score** | ${team.overallScore.toFixed(1)} % |\n`; - md += `| **Status** | ${healthLabel} |\n`; - md += `| **Repos** | ${team.repoCount} |\n`; - if (team.slackChannel) md += `| **Slack** | ${team.slackChannel} |\n`; - if (team.members && team.members.length > 0) { - md += `| **Medlemmer** | ${team.members.join(", ")} |\n`; - } - md += "\n"; - - // Kategoriscorer - md += `## Kategoriscorer\n\n`; - md += `| Kategori | Score |\n|---|---|\n`; - const catLabels = { sikkerhet: "Sikkerhet", devops: "DevOps-modenhet", governance: "Governance" }; - for (const [cat, label] of Object.entries(catLabels)) { - const score = team.categoryScores[cat]; - md += `| ${label} | ${score !== null && score !== undefined ? score.toFixed(1) + " %" : "–"} |\n`; - } - md += "\n"; - - // Sjekk-oversikt - md += `## Sjekk-oversikt\n\n`; - md += `| Sjekk | Bestått | Feilet | N/A | Score |\n|---|---|---|---|---|\n`; - for (const chk of checks) { - const stat = team.byCheck[chk.id]; - if (!stat) continue; - const scoreStr = stat.score !== null && stat.score !== undefined - ? stat.score.toFixed(1) + " %" - : "–"; - md += `| ${chk.label} | ${stat.passed} | ${stat.failed} | ${stat.na} | ${scoreStr} |\n`; - } - md += "\n"; - - // Sårbarhetsoversikt - md += `## Sårbarheter\n\n`; - md += `- **Totalt:** ${team.vulnerabilities.total}\n`; - md += `- **Kritisk:** ${team.vulnerabilities.critical}\n`; - md += `- **Høy:** ${team.vulnerabilities.high}\n`; - md += `- **Middels:** ${team.vulnerabilities.medium}\n`; - md += `- **Lav:** ${team.vulnerabilities.low}\n\n`; - - // Topp-5 kritiske CVEer - const critVulns = []; - for (const repo of repoResults) { - for (const v of (repo.vulnerabilities || [])) { - if ((v.severity || "").toUpperCase() === "CRITICAL") { - critVulns.push({ ...v, _repo: `${repo.project}/${repo.repo}` }); - } - } - } - critVulns.sort((a, b) => (b.cvssScore || 0) - (a.cvssScore || 0)); - - if (critVulns.length > 0) { - md += `### Topp kritiske sårbarheter\n\n`; - md += `| CVE | Pakke | Versjon | Repo |\n|---|---|---|---|\n`; - for (const v of critVulns.slice(0, 5)) { - md += `| ${v.cveId || v.id} | ${v.package} | ${v.version} | ${v._repo} |\n`; - } - md += "\n"; - } - - // Anbefalte tiltak — sortert etter lavest score - const actionItems = checks - .map((chk) => { - const stat = team.byCheck[chk.id]; - if (!stat || stat.failed === 0) return null; - return { label: chk.label, failed: stat.failed, score: stat.score }; - }) - .filter(Boolean) - .sort((a, b) => (a.score || 0) - (b.score || 0)); - - if (actionItems.length > 0) { - md += `## Anbefalte tiltak\n\n`; - for (const item of actionItems) { - const scoreStr = item.score !== null ? item.score.toFixed(1) + " %" : "–"; - md += `1. **${item.label}** — ${item.failed} repo(er) feilet (score: ${scoreStr})\n`; - } - md += "\n"; - } - - return md; -} - -module.exports = { buildTeamMarkdownReport }; diff --git a/bitbucket-audit/lib/teamConfig.js b/bitbucket-audit/lib/teamConfig.js deleted file mode 100644 index 98158a7..0000000 --- a/bitbucket-audit/lib/teamConfig.js +++ /dev/null @@ -1,205 +0,0 @@ -"use strict"; - -// --------------------------------------------------------------------------- -// Sjekk-kategorier — brukes ved beregning av kategoriscore -// --------------------------------------------------------------------------- - -const CHECK_CATEGORIES = { - sikkerhet: ["secrets", "branch-protection", "dep-vulns", "npm-audit", "owasp-dep-check"], - devops: ["pipeline", "renovate", "linting", "tests", "pr-activity"], - governance: ["readme", "stale", "codeowners"], -}; - -// --------------------------------------------------------------------------- -// loadTeamConfig — les og valider teams.json -// --------------------------------------------------------------------------- - -function loadTeamConfig(filePath) { - const fs = require("fs"); - - let raw; - try { - raw = fs.readFileSync(filePath, "utf8"); - } catch (err) { - throw new Error(`Kunne ikke lese teams.json: ${err.message}`); - } - - let config; - try { - config = JSON.parse(raw); - } catch (err) { - throw new Error(`Ugyldig JSON i teams.json: ${err.message}`); - } - - if (!config.teams || !Array.isArray(config.teams)) { - throw new Error("teams.json mangler 'teams'-array."); - } - - const teamIds = new Set(); - for (const team of config.teams) { - if (!team.id) throw new Error(`Et team mangler påkrevd felt 'id'.`); - if (!team.name) throw new Error(`Team '${team.id}' mangler påkrevd felt 'name'.`); - if (teamIds.has(team.id)) { - throw new Error(`Duplisert team-ID funnet: '${team.id}'. Hver team-ID må være unik.`); - } - teamIds.add(team.id); - } - - return config; -} - -// --------------------------------------------------------------------------- -// assignReposToTeams — kart repoKey → teamId for alle repos -// --------------------------------------------------------------------------- - -function assignReposToTeams(repos, teamConfig) { - // Bygg eksplisitt repo-til-team-mapping (høyest prioritet) - const explicitMap = new Map(); - for (const team of teamConfig.teams) { - if (!team.repos) continue; - for (const entry of team.repos) { - const key = `${entry.project}/${entry.repo}`; - if (explicitMap.has(key)) { - throw new Error( - `Repo '${key}' er tilordnet til to team: '${explicitMap.get(key)}' og '${team.id}'. ` + - `Et repo kan kun tilhøre ett team.` - ); - } - explicitMap.set(key, team.id); - } - } - - const assignment = new Map(); - for (const repo of repos) { - const key = `${repo.project}/${repo.repo}`; - - // Prioritet 1: eksplisitt repos[]-liste - if (explicitMap.has(key)) { - assignment.set(key, explicitMap.get(key)); - continue; - } - - // Prioritet 2: projects[]-liste — matcher alle repos i et Bitbucket-prosjekt - let found = false; - for (const team of teamConfig.teams) { - if (team.projects && team.projects.includes(repo.project)) { - assignment.set(key, team.id); - found = true; - break; - } - } - - // Prioritet 3: ukjent → unassigned - if (!found) { - assignment.set(key, "unassigned"); - } - } - - return assignment; -} - -// --------------------------------------------------------------------------- -// buildTeamReport — bygg teams[]-seksjonen for rapport-JSON -// --------------------------------------------------------------------------- - -function buildTeamReport(repoResults, teamConfig, checks) { - const assignment = assignReposToTeams(repoResults, teamConfig); - - // Grupper repo-resultater per team - const teamReposMap = new Map(); - for (const repo of repoResults) { - const key = `${repo.project}/${repo.repo}`; - const teamId = assignment.get(key) || "unassigned"; - if (!teamReposMap.has(teamId)) teamReposMap.set(teamId, []); - teamReposMap.get(teamId).push(repo); - } - - const teams = []; - - // Bygg data for konfigurerte team - for (const team of teamConfig.teams) { - const repos = teamReposMap.get(team.id) || []; - teams.push(_buildTeamEntry( - team.id, team.name, team.description || "", - team.slackChannel || null, team.members || [], - repos, checks - )); - } - - // Bygg "Ikke tilordnet"-team for repos uten match - const unassignedRepos = teamReposMap.get("unassigned") || []; - if (unassignedRepos.length > 0) { - teams.push(_buildTeamEntry( - "unassigned", "Ikke tilordnet", - "Repos som ikke er tilordnet noe team.", - null, [], unassignedRepos, checks - )); - } - - return teams; -} - -// --------------------------------------------------------------------------- -// Intern hjelper: bygg ett team-oppslag -// --------------------------------------------------------------------------- - -function _buildTeamEntry(id, name, description, slackChannel, members, repos, checks) { - const byCheck = {}; - - for (const chk of checks) { - const passed = repos.filter((r) => r.checks[chk.id] === true).length; - const na = repos.filter((r) => r.checks[chk.id] === null).length; - const failed = repos.filter((r) => r.checks[chk.id] === false).length; - const applicable = repos.length - na; - const score = applicable > 0 ? +((passed / applicable) * 100).toFixed(1) : null; - byCheck[chk.id] = { passed, failed, na, score }; - } - - // Kategoriscore = uvektet gjennomsnitt av sjekker i kategorien (NA-sjekker ekskludert) - const categoryScores = {}; - for (const [cat, checkIds] of Object.entries(CHECK_CATEGORIES)) { - const scores = checkIds - .map((id) => byCheck[id]?.score) - .filter((s) => s !== null && s !== undefined); - categoryScores[cat] = scores.length > 0 - ? +(scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) - : null; - } - - // Samlet score = uvektet gjennomsnitt av alle sjekker - const allScores = Object.values(byCheck) - .map((c) => c.score) - .filter((s) => s !== null && s !== undefined); - const overallScore = allScores.length > 0 - ? +(allScores.reduce((a, b) => a + b, 0) / allScores.length).toFixed(1) - : 0; - - // Summer sårbarheter på tvers av teamets repos - const vulnerabilities = { total: 0, critical: 0, high: 0, medium: 0, low: 0 }; - for (const repo of repos) { - for (const v of (repo.vulnerabilities || [])) { - vulnerabilities.total++; - const sev = (v.severity || "").toUpperCase(); - if (sev === "CRITICAL") vulnerabilities.critical++; - else if (sev === "HIGH") vulnerabilities.high++; - else if (sev === "MEDIUM") vulnerabilities.medium++; - else if (sev === "LOW") vulnerabilities.low++; - } - } - - return { - id, - name, - description, - slackChannel, - members, - repoCount: repos.length, - overallScore, - categoryScores, - byCheck, - vulnerabilities, - repos: repos.map((r) => `${r.project}/${r.repo}`), - }; -} - -module.exports = { loadTeamConfig, assignReposToTeams, buildTeamReport, CHECK_CATEGORIES }; diff --git a/bitbucket-audit/lib/teamOwnership.js b/bitbucket-audit/lib/teamOwnership.js deleted file mode 100644 index 16732a0..0000000 --- a/bitbucket-audit/lib/teamOwnership.js +++ /dev/null @@ -1,139 +0,0 @@ -"use strict"; - -// --------------------------------------------------------------------------- -// Team-eierskap: berik rapport med team-felt basert på mapping hentet via URL -// --------------------------------------------------------------------------- - -/** - * Hent team-eierskap fra en absolutt URL. - * Returnerer null hvis url er null. - * Kaster Error ved HTTP-feil eller ugyldig JSON. - * - * JSON-struktur: { [product]: { [teamId]: [ { project, slug } ] } } - * - * @param {string|null} url - Absolutt URL til JSON-mapping - * @param {Function} request - request()-funksjonen fra http.js - * @returns {Promise} - */ -async function loadOwnership(url, request) { - if (!url) return null; - return request(url); -} - -/** - * Bygg oppslagstabell fra ownership-JSON. - * Nøkkel: "PROJECT/slug" Verdi: { product, teamId } - * Første match vinner ved duplikater. - * - * @param {Object} ownershipJson - { [product]: { [teamId]: [ { project, slug } ] } } - * @returns {Map} - */ -function buildLookupMap(ownershipJson) { - const map = new Map(); - for (const [product, teams] of Object.entries(ownershipJson)) { - for (const [teamId, repos] of Object.entries(teams)) { - if (!Array.isArray(repos)) continue; - for (const { project, slug } of repos) { - if (!project || !slug) continue; - const key = `${project}/${slug}`; - if (!map.has(key)) { - map.set(key, { product, teamId }); - } - } - } - } - return map; -} - -/** - * Berik hvert repo med et 'team'-felt basert på oppslagstabellen. - * Setter team = { product, id } eller team = null dersom ingen match. - * - * @param {Array} repos - report.repos - * @param {Map} lookupMap - fra buildLookupMap() - */ -function enrichRepos(repos, lookupMap) { - for (const repo of repos) { - const key = `${repo.project}/${repo.repo}`; - const match = lookupMap.get(key); - repo.team = match ? { product: match.product, id: match.teamId } : null; - } -} - -/** - * Bygg byTeam-oversikt for summary-seksjonen. - * Nøkkel: "product/teamId" (compound for å unngå kollisjoner på tvers av produkter). - * Inkluderer "unassigned"-gruppe for repos uten treff. - * - * @param {Array} repos - report.repos (allerede beriket med .team) - * @param {Array} checks - Array av sjekk-objekter med .id - * @param {Object} ownershipJson - { [product]: { [teamId]: [...] } } - * @returns {Object} byTeam - */ -function buildByTeam(repos, checks, ownershipJson) { - // Bygg opp teamMap: compound-nøkkel → { name, repos[] } - const teamMap = new Map(); - - // Registrer kjente team i riktig rekkefølge - for (const [product, teams] of Object.entries(ownershipJson)) { - for (const teamId of Object.keys(teams)) { - const key = `${product}/${teamId}`; - if (!teamMap.has(key)) { - teamMap.set(key, { name: key, repos: [] }); - } - } - } - - // Sorter repos inn i riktig bøtte - for (const repo of repos) { - if (repo.team) { - const key = `${repo.team.product}/${repo.team.id}`; - if (!teamMap.has(key)) { - teamMap.set(key, { name: key, repos: [] }); - } - teamMap.get(key).repos.push(repo); - } else { - if (!teamMap.has("unassigned")) { - teamMap.set("unassigned", { name: "Unassigned", repos: [] }); - } - teamMap.get("unassigned").repos.push(repo); - } - } - - // Beregn statistikk per team - const byTeam = {}; - - for (const [teamKey, { name, repos: teamRepos }] of teamMap) { - if (teamRepos.length === 0) continue; - - const total = teamRepos.length; - const byCheck = {}; - - for (const chk of checks) { - const passed = teamRepos.filter((r) => r.checks[chk.id] === true).length; - const notApplicable = teamRepos.filter((r) => r.checks[chk.id] === null).length; - const coveredByAlt = teamRepos.filter( - (r) => - r.checks[chk.id] === false && - r.assessments && - r.assessments[chk.id] && - r.assessments[chk.id].startsWith("Ikke nødvendig") - ).length; - const failed = teamRepos.filter((r) => r.checks[chk.id] === false).length - coveredByAlt; - const applicable = total - notApplicable; - const covered = passed + coveredByAlt; - - byCheck[chk.id] = { - passed, - failed, - coveragePercent: applicable ? +((covered / applicable) * 100).toFixed(1) : 0, - }; - } - - byTeam[teamKey] = { name, repoCount: total, byCheck }; - } - - return byTeam; -} - -module.exports = { loadOwnership, buildLookupMap, enrichRepos, buildByTeam }; diff --git a/frontend/app.js b/frontend/app.js index ee6d3b0..19ae6d5 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -47,6 +47,12 @@ window.exportTeamReport = exportTeamReport; // Event-lyttere // --------------------------------------------------------------------------- document.addEventListener("DOMContentLoaded", () => { + // Last inn team-konfig fra teams.json (valgfri statisk fil i frontend-roten) + fetch("teams.json") + .then((r) => r.ok ? r.json() : null) + .then((json) => { if (json?.teams) state.teamsConfig = json; }) + .catch(() => {}); // Ignorer feil — teams.json er valgfri + // Navigasjon $$(".nav-btn[data-view]").forEach(btn => { btn.addEventListener("click", () => switchView(btn.dataset.view)); diff --git a/frontend/js/data/report.js b/frontend/js/data/report.js index 5d6f0fe..8c57ba1 100644 --- a/frontend/js/data/report.js +++ b/frontend/js/data/report.js @@ -8,6 +8,7 @@ import { CHECK_LABELS, CHECK_ICONS } from "../constants/checkLabels.js"; import { $, toast } from "../utils/dom.js"; import { formatDate } from "../utils/format.js"; import { renderActiveView } from "../views/router.js"; +import { buildTeamsFromConfig } from "./teamData.js"; /** Les en JSON-fil og lever den til loadReport hvis den er gyldig. */ export function handleFile(file) { @@ -54,89 +55,15 @@ export function loadReport(data) { } // --------------------------------------------------------------------------- -// Normalisering av nytt team-format (summary.byTeam + repo.team) +// Normalisering av team-data // --------------------------------------------------------------------------- -const CHECK_CATEGORIES_LOCAL = { - sikkerhet: ["secrets", "branch-protection", "dep-vulns", "npm-audit", "owasp-dep-check"], - devops: ["pipeline", "renovate", "linting", "tests", "pr-activity"], - governance: ["readme", "stale", "codeowners"], -}; - /** - * Konverterer nytt rapportformat (summary.byTeam + repo.team-felt) til det - * interne team-formatet (data.teams-array) som resten av frontenden forventer. - * Gjør ingenting hvis data.teams allerede finnes, eller byTeam mangler. + * Bygger data.teams[]-arrayen fra teams.json-konfig og rapport-repos. + * Gjør ingenting hvis data.teams allerede finnes (bakoverkompatibilitet). */ function normalizeTeams(data) { if (Array.isArray(data.teams) && data.teams.length > 0) return; - const byTeam = data.summary?.byTeam; - if (!byTeam || Object.keys(byTeam).length === 0) return; - - // Bygg repo-lookup: teamKey → ["PROJECT/repo", ...] - const teamReposMap = new Map(); - for (const repo of (data.repos || [])) { - if (!repo.team) continue; - const key = `${repo.team.product}/${repo.team.id}`; - if (!teamReposMap.has(key)) teamReposMap.set(key, []); - teamReposMap.get(key).push(`${repo.project}/${repo.repo}`); - } - - data.teams = Object.entries(byTeam).map(([teamKey, entry]) => { - // Adapter byCheck: coveragePercent → score, legg til na: 0 - const byCheck = {}; - for (const [checkId, stat] of Object.entries(entry.byCheck || {})) { - byCheck[checkId] = { - passed: stat.passed, - failed: stat.failed, - na: 0, - score: stat.coveragePercent ?? null, - }; - } - - // overallScore = snitt av alle score-verdier - const scores = Object.values(byCheck) - .map(s => s.score) - .filter(s => s !== null && s !== undefined); - const overallScore = scores.length > 0 - ? parseFloat((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)) - : 0; - - // categoryScores = snitt per kategori - const categoryScores = {}; - for (const [cat, ids] of Object.entries(CHECK_CATEGORIES_LOCAL)) { - const catScores = ids - .map(id => byCheck[id]?.score) - .filter(s => s !== null && s !== undefined); - categoryScores[cat] = catScores.length > 0 - ? parseFloat((catScores.reduce((a, b) => a + b, 0) / catScores.length).toFixed(1)) - : null; - } - - // vulnerabilities = telle fra matchede repos - const repoKeys = teamReposMap.get(teamKey) || []; - const repoKeySet = new Set(repoKeys); - let totalVulns = 0, criticalVulns = 0; - for (const repo of (data.repos || [])) { - if (!repoKeySet.has(`${repo.project}/${repo.repo}`)) continue; - for (const v of (repo.vulnerabilities || [])) { - totalVulns++; - if ((v.severity || "").toUpperCase() === "CRITICAL") criticalVulns++; - } - } - - return { - id: teamKey, - name: entry.name, - description: null, - repoCount: entry.repoCount, - repos: repoKeys, - overallScore, - categoryScores, - byCheck, - vulnerabilities: { total: totalVulns, critical: criticalVulns }, - members: [], - slackChannel: null, - }; - }); + if (!state.teamsConfig) return; + data.teams = buildTeamsFromConfig(data.repos || [], state.teamsConfig, data.checks || []); } diff --git a/frontend/js/data/teamData.js b/frontend/js/data/teamData.js index 7aa07a1..d4ab468 100644 --- a/frontend/js/data/teamData.js +++ b/frontend/js/data/teamData.js @@ -51,3 +51,141 @@ export function getTeamForRepo(projectKey, repoSlug) { } return null; } + +// --------------------------------------------------------------------------- +// Team-konfig fra teams.json — tilordning og statistikkbygging +// --------------------------------------------------------------------------- + +/** + * Bygg Map fra "PROJECT/slug" → teamId basert på teams.json-konfig. + * Prioritet: eksplisitt repos[] > projects[] > "unassigned" + */ +export function assignReposToTeams(repos, teamsConfig) { + // Bygg eksplisitt repo-til-team-mapping (høyest prioritet) + const explicitMap = new Map(); + for (const team of teamsConfig.teams) { + if (!team.repos) continue; + for (const entry of team.repos) { + const key = `${entry.project}/${entry.repo}`; + if (!explicitMap.has(key)) explicitMap.set(key, team.id); + } + } + + const assignment = new Map(); + for (const repo of repos) { + const key = `${repo.project}/${repo.repo}`; + + if (explicitMap.has(key)) { + assignment.set(key, explicitMap.get(key)); + continue; + } + + let found = false; + for (const team of teamsConfig.teams) { + if (team.projects && team.projects.includes(repo.project)) { + assignment.set(key, team.id); + found = true; + break; + } + } + + if (!found) assignment.set(key, "unassigned"); + } + + return assignment; +} + +/** + * Bygg data.teams[]-array fra repos og teams.json-konfig. + * checkIds er arrayen fra rapport-JSON: ["secrets", "pipeline", ...] + */ +export function buildTeamsFromConfig(repos, teamsConfig, checkIds) { + const assignment = assignReposToTeams(repos, teamsConfig); + + // Grupper repos per team + const teamReposMap = new Map(); + for (const repo of repos) { + const key = `${repo.project}/${repo.repo}`; + const teamId = assignment.get(key) || "unassigned"; + if (!teamReposMap.has(teamId)) teamReposMap.set(teamId, []); + teamReposMap.get(teamId).push(repo); + } + + const teams = []; + + for (const team of teamsConfig.teams) { + const teamRepos = teamReposMap.get(team.id) || []; + teams.push(_buildTeamEntry( + team.id, team.name, team.description || null, + team.slackChannel || null, team.members || [], + teamRepos, checkIds + )); + } + + const unassignedRepos = teamReposMap.get("unassigned") || []; + if (unassignedRepos.length > 0) { + teams.push(_buildTeamEntry( + "unassigned", "Ikke tilordnet", + "Repos som ikke er tilordnet noe team.", + null, [], unassignedRepos, checkIds + )); + } + + return teams; +} + +function _buildTeamEntry(id, name, description, slackChannel, members, repos, checkIds) { + const byCheck = {}; + + for (const checkId of checkIds) { + const passed = repos.filter((r) => r.checks[checkId] === true).length; + const na = repos.filter((r) => r.checks[checkId] === null).length; + const failed = repos.filter((r) => r.checks[checkId] === false).length; + const applicable = repos.length - na; + const score = applicable > 0 ? +((passed / applicable) * 100).toFixed(1) : null; + byCheck[checkId] = { passed, failed, na, score }; + } + + const categoryScores = {}; + for (const [cat, ids] of Object.entries(CHECK_CATEGORIES)) { + const scores = ids + .map((cid) => byCheck[cid]?.score) + .filter((s) => s !== null && s !== undefined); + categoryScores[cat] = scores.length > 0 + ? +(scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) + : null; + } + + const allScores = Object.values(byCheck) + .map((c) => c.score) + .filter((s) => s !== null && s !== undefined); + const overallScore = allScores.length > 0 + ? +(allScores.reduce((a, b) => a + b, 0) / allScores.length).toFixed(1) + : 0; + + const vulnerabilities = { total: 0, critical: 0, high: 0, medium: 0, low: 0 }; + for (const repo of repos) { + for (const v of (repo.vulnerabilities || [])) { + vulnerabilities.total++; + const sev = (v.severity || "").toUpperCase(); + if (sev === "CRITICAL") vulnerabilities.critical++; + else if (sev === "HIGH") vulnerabilities.high++; + else if (sev === "MEDIUM") vulnerabilities.medium++; + else if (sev === "LOW") vulnerabilities.low++; + } + } + + return { + id, + name, + description, + slackChannel, + members, + repoCount: repos.length, + overallScore, + categoryScores, + byCheck, + vulnerabilities, + repos: repos.map((r) => `${r.project}/${r.repo}`), + }; +} diff --git a/frontend/js/state.js b/frontend/js/state.js index 252edec..3bc8f2f 100644 --- a/frontend/js/state.js +++ b/frontend/js/state.js @@ -19,6 +19,8 @@ export const state = { activeView: "summary", /** Om den lastede rapporten inneholder team-data. Styrer synlighet av Team-UI. */ hasTeams: false, + /** Innlastet teams.json-konfig (null hvis ikke tilgjengelig). */ + teamsConfig: null, /** Aktiv team-ID for detaljvisning (null = team-liste). */ activeTeam: null, /** Filtre og sortering i Teams-fanen. */ diff --git a/bitbucket-audit/teams.example.json b/frontend/teams.example.json similarity index 100% rename from bitbucket-audit/teams.example.json rename to frontend/teams.example.json