Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions bitbucket-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ Etter første kjøring hentes tokenet automatisk. Du kan fortsatt overstyre ved

| Variabel | Kilde | Beskrivelse |
| ----------------- | --------------- | ------------------------------------------------------ |
| `BITBUCKET_URL` | `.env` / shell | Base-URL til Bitbucket Server/Data Center (**påkrevd**)|
| `BITBUCKET_TOKEN` | Sikker lagring | Personal Access Token med `PROJECT_READ`/`REPO_READ` |
| `CONCURRENCY` | `.env` / shell | Antall samtidige repo-sjekker (default `5`) |
| `MAX_REPOS` | `.env` / shell | Maks antall repos å sjekke, sortert alfabetisk (default `0` = ingen grense) |
| `BITBUCKET_URL` | `.env` / shell | Base-URL til Bitbucket Server/Data Center (**påkrevd**)|
| `BITBUCKET_TOKEN` | Sikker lagring | Personal Access Token med `PROJECT_READ`/`REPO_READ` |
| `CONCURRENCY` | `.env` / shell | Antall samtidige repo-sjekker (default `5`) |
| `MAX_REPOS` | `.env` / shell | Maks antall repos å sjekke, sortert alfabetisk (default `0` = ingen grense) |
| `TEAM_REPOS_MAPPING` | `.env` / shell | Absolutt URL til JSON-fil med product/team/repo-mapping (valgfri) |

## Kjør

Expand Down Expand Up @@ -172,6 +173,56 @@ Det er alt. Resten av systemet plukker opp sjekken automatisk.
| `STALE_MONTHS` | `12` | Antall måneder uten commit før repoet regnes som inaktivt |
| `PR_MONTHS` | `6` | Tidsvindu (måneder) for å vurdere PR-aktivitet |

## Team-eierskap

Argus støtter valgfri team-mapping via en JSON-fil publisert på en HTTP(S)-URL (f.eks. i Bitbucket). Sett miljøvariabelen `TEAM_REPOS_MAPPING` i `.env` til den absolutte URL-en:

```dotenv
TEAM_REPOS_MAPPING=https://bitbucket.eksempel.no/raw/team-repos-mapping.json
```

JSON-strukturen grupperer repos under produkt og team:

```json
{
"akr": {
"ferrari": [
{ "project": "ATLAS", "slug": "atlas-api" },
{ "project": "ATLAS", "slug": "atlas-worker" }
],
"lamborghini": [
{ "project": "VEG", "slug": "vegkart" }
]
}
}
```

Oppslag skjer på `project` + `slug` (ikke bare prosjektnøkkel). Første match vinner ved duplikater.

Når `TEAM_REPOS_MAPPING` er satt, beriker Argus rapporten automatisk:

- Hvert repo-objekt får et `team`-felt: `{ "product": "akr", "id": "ferrari" }` — eller `null` om repoet ikke er kartlagt.
- `summary.byTeam` legges til med per-team statistikk. Nøkkelen er `"product/teamId"` (compound) for å unngå kollisjoner på tvers av produkter. Repos uten treff samles i en `"unassigned"`-gruppe.

```json
"byTeam": {
"akr/ferrari": {
"name": "akr/ferrari",
"repoCount": 12,
"byCheck": {
"renovate": { "passed": 8, "failed": 4, "coveragePercent": 66.7 }
}
},
"unassigned": {
"name": "Unassigned",
"repoCount": 5,
"byCheck": { ... }
}
}
```

Er `TEAM_REPOS_MAPPING` ikke satt, fortsetter Argus som normalt uten feil og uten `team`-felt i rapporten.

## Rapportformat

Rapporten skrives til `audit-report.json` med følgende struktur:
Expand All @@ -191,6 +242,7 @@ Rapporten skrives til `audit-report.json` med følgende struktur:
{
"project": "PLATTFORM",
"repo": "atlas-api",
"team": { "product": "akr", "id": "ferrari" },
"checks": { "renovate": true, "owasp-dep-check": false },
"assessments": {
"owasp-dep-check": "Anbefalt — har Jenkinsfile og avhengigheter (package.json), men OWASP Dependency-Check mangler i pipeline."
Expand Down
2 changes: 1 addition & 1 deletion bitbucket-audit/checks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = [
require("./depVulns"),
require("./codeowners"),
require("./pipeline"),
require("./branchProtection"),
//require("./branchProtection"),
require("./secrets"),
require("./stale"),
require("./readme"),
Expand Down
25 changes: 25 additions & 0 deletions bitbucket-audit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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");

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -48,6 +49,20 @@ 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) {
Expand Down Expand Up @@ -182,6 +197,16 @@ 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 });

Expand Down
1 change: 1 addition & 0 deletions bitbucket-audit/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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() {
Expand Down
139 changes: 139 additions & 0 deletions bitbucket-audit/lib/teamOwnership.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"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<Object|null>}
*/
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<string, { product: string, teamId: string }>}
*/
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 };