codeowners-audit
Advanced tools
+3
-1
| { | ||
| "name": "codeowners-audit", | ||
| "version": "2.6.0", | ||
| "version": "2.7.0", | ||
| "description": "Generate an HTML report for CODEOWNERS ownership gaps and run in CI or from the CLI to fail when files are not covered.", | ||
@@ -10,2 +10,4 @@ "type": "module", | ||
| "scripts": { | ||
| "lint:commit": "commitlint --last --verbose", | ||
| "lint:commit:message": "commitlint --verbose", | ||
| "test": "node --test", | ||
@@ -12,0 +14,0 @@ "update-example": "node report.js nodejs/node --no-open -o example.html" |
+152
-3
@@ -254,2 +254,12 @@ <!doctype html> | ||
| } | ||
| .warning-owner-link { | ||
| color: inherit; | ||
| text-decoration: underline; | ||
| text-decoration-color: rgba(103, 232, 249, 0.45); | ||
| text-underline-offset: 2px; | ||
| } | ||
| .warning-owner-link:hover { | ||
| color: #a5f3fc; | ||
| text-decoration-color: rgba(165, 243, 252, 0.7); | ||
| } | ||
| .warning-text { | ||
@@ -262,2 +272,21 @@ color: var(--muted); | ||
| } | ||
| .warning-history { | ||
| margin-left: 8px; | ||
| white-space: nowrap; | ||
| } | ||
| .warning-history-link, | ||
| .warning-history-text { | ||
| color: #c4b5fd; | ||
| font-size: 12px; | ||
| font-weight: 600; | ||
| } | ||
| .warning-history-link { | ||
| text-decoration: underline; | ||
| text-decoration-color: rgba(196, 181, 253, 0.45); | ||
| text-underline-offset: 2px; | ||
| } | ||
| .warning-history-link:hover { | ||
| color: #ddd6fe; | ||
| text-decoration-color: rgba(221, 214, 254, 0.7); | ||
| } | ||
@@ -510,2 +539,5 @@ table { | ||
| const clamp = value => Math.max(0, Math.min(100, value)) | ||
| const reportGeneratedAtMs = Number.isFinite(Date.parse(report.generatedAt)) | ||
| ? Date.parse(report.generatedAt) | ||
| : Date.now() | ||
| const scopeQueryParam = 'scope' | ||
@@ -635,7 +667,6 @@ const directoryRows = report.directories.filter(row => row.path !== '(root)') | ||
| ownersSpan.className = 'warning-owners' | ||
| ownersSpan.textContent = Array.isArray(warning.owners) && warning.owners.length > 0 | ||
| ? warning.owners.join(', ') | ||
| : '(none)' | ||
| appendCodeownersOwnerList(ownersSpan, warning.owners) | ||
| item.appendChild(ownersSpan) | ||
| appendMissingPathWarningHistory(item, warning) | ||
| list.appendChild(item) | ||
@@ -647,2 +678,120 @@ } | ||
| function appendMissingPathWarningHistory (target, warning) { | ||
| const history = warning && typeof warning.history === 'object' ? warning.history : null | ||
| if (!history || typeof history.addedAt !== 'string') { | ||
| return | ||
| } | ||
| const ageLabel = formatRelativeAge(history.addedAt) | ||
| const historyNode = history.commitUrl | ||
| ? createHistoryLinkNode(ageLabel, history) | ||
| : createHistoryTextNode(ageLabel, history) | ||
| const wrapper = document.createElement('span') | ||
| wrapper.className = 'warning-history' | ||
| wrapper.appendChild(document.createTextNode(' \u00b7 ')) | ||
| wrapper.appendChild(historyNode) | ||
| target.appendChild(wrapper) | ||
| } | ||
| function appendCodeownersOwnerList (target, owners) { | ||
| target.textContent = '' | ||
| if (!Array.isArray(owners) || owners.length === 0) { | ||
| target.textContent = '(none)' | ||
| return | ||
| } | ||
| owners.forEach((owner, index) => { | ||
| if (index > 0) { | ||
| target.appendChild(document.createTextNode(', ')) | ||
| } | ||
| target.appendChild(createCodeownersOwnerNode(owner)) | ||
| }) | ||
| } | ||
| function createCodeownersOwnerNode (owner) { | ||
| const label = typeof owner === 'string' ? owner.trim() : '' | ||
| const href = getCodeownersOwnerGithubUrl(label) | ||
| if (!href) { | ||
| return document.createTextNode(label || '(unknown)') | ||
| } | ||
| const link = document.createElement('a') | ||
| link.className = 'warning-owner-link' | ||
| link.href = href | ||
| link.target = '_blank' | ||
| link.rel = 'noopener noreferrer' | ||
| link.textContent = label | ||
| return link | ||
| } | ||
| function getCodeownersOwnerGithubUrl (owner) { | ||
| if (typeof owner !== 'string') return '' | ||
| const normalized = owner.trim() | ||
| if (!normalized.startsWith('@')) return '' | ||
| const segments = normalized.slice(1).split('/') | ||
| if (segments.some(segment => !segment)) return '' | ||
| if (segments.length === 1) { | ||
| return 'https://github.com/' + encodeURIComponent(segments[0]) | ||
| } | ||
| if (segments.length === 2) { | ||
| return 'https://github.com/orgs/' + encodeURIComponent(segments[0]) + | ||
| '/teams/' + encodeURIComponent(segments[1]) | ||
| } | ||
| return '' | ||
| } | ||
| function createHistoryLinkNode (ageLabel, history) { | ||
| const link = document.createElement('a') | ||
| link.className = 'warning-history-link' | ||
| link.href = history.commitUrl | ||
| link.target = '_blank' | ||
| link.rel = 'noopener noreferrer' | ||
| link.textContent = ageLabel | ||
| link.title = buildHistoryTitle(history) | ||
| return link | ||
| } | ||
| function createHistoryTextNode (ageLabel, history) { | ||
| const label = document.createElement('span') | ||
| label.className = 'warning-history-text' | ||
| label.textContent = ageLabel | ||
| label.title = buildHistoryTitle(history) | ||
| return label | ||
| } | ||
| function buildHistoryTitle (history) { | ||
| const addedAt = Number.isFinite(Date.parse(history.addedAt)) | ||
| ? new Date(history.addedAt).toLocaleString() | ||
| : history.addedAt | ||
| const shortSha = typeof history.commitSha === 'string' && history.commitSha | ||
| ? ' (' + history.commitSha.slice(0, 7) + ')' | ||
| : '' | ||
| return 'Added ' + addedAt + shortSha | ||
| } | ||
| function formatRelativeAge (value) { | ||
| const timestamp = Date.parse(value) | ||
| if (!Number.isFinite(timestamp)) return 'history' | ||
| const elapsedMs = Math.max(0, reportGeneratedAtMs - timestamp) | ||
| const minuteMs = 60 * 1000 | ||
| const hourMs = 60 * minuteMs | ||
| const dayMs = 24 * hourMs | ||
| const weekMs = 7 * dayMs | ||
| const monthMs = 30 * dayMs | ||
| const yearMs = 365 * dayMs | ||
| if (elapsedMs >= yearMs) return Math.floor(elapsedMs / yearMs) + 'y ago' | ||
| if (elapsedMs >= monthMs) return Math.floor(elapsedMs / monthMs) + 'mo ago' | ||
| if (elapsedMs >= weekMs) return Math.floor(elapsedMs / weekMs) + 'w ago' | ||
| if (elapsedMs >= dayMs) return Math.floor(elapsedMs / dayMs) + 'd ago' | ||
| if (elapsedMs >= hourMs) return Math.floor(elapsedMs / hourMs) + 'h ago' | ||
| if (elapsedMs >= minuteMs) return Math.floor(elapsedMs / minuteMs) + 'm ago' | ||
| return 'just now' | ||
| } | ||
| function renderCodeownersDiscoveryWarnings (validationMeta) { | ||
@@ -649,0 +798,0 @@ const container = document.getElementById('codeowners-discovery-warnings') |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
156843
8.08%3061
7.22%19
11.76%