Skip to content

ComplianceScore in Get-ComplianceReportData is always 0% or 100% — TotalDependencies counts violations, not scanned dependencies #929

@WilliamBerryiii

Description

@WilliamBerryiii

Summary

Get-ComplianceReportData in scripts/security/Test-DependencyPinning.ps1 produces a compliance score that is always either 0% or 100%, never a meaningful partial value. The root cause is that TotalDependencies is set to the violation count rather than the total number of scanned dependencies, and a dead-code severity filter (-ne 'Info') masks the problem.

Root Cause

The function computes compliance like this (lines 779–793):

$totalDeps = @($Violations).Count
$unpinnedDeps = @($Violations | Where-Object { $_.Severity -ne 'Info' }).Count
$pinnedDeps = $totalDeps - $unpinnedDeps
# ...
$report.ComplianceScore = [math]::Round(($pinnedDeps / $totalDeps) * 100, 2)

Problem 1 — TotalDependencies equals violation count, not dependency count.
The function only receives $Violations and $ScannedFiles, with no information about total scanned dependencies. It sets TotalDependencies = @($Violations).Count, so the denominator is only ever the number of failures. The ComplianceReport class (scripts/security/Modules/SecurityClasses.psm1) was designed for partial compliance — CalculateScore() computes PinnedDependencies / TotalDependencies — but the function never provides a real total, rendering the formula meaningless.

Problem 2 — Dead-code severity filter.
The Where-Object { $_.Severity -ne 'Info' } filter is intended to exclude informational items from the unpinned count and treat them as "pinned." However, all three DependencyViolation creation sites in the codebase assign only 'Medium' or 'High':

Location Line Severity
Get-DownloadDependencyViolations ~377 'Medium'
Get-NpmDependencyViolations ~466 'Medium'
Generic pattern matching ~693 'High' (github-actions) / 'Medium' (other)

Because no violation ever carries 'Info' severity, $unpinnedDeps always equals $totalDeps, $pinnedDeps is always 0, and the score is always 0.0% when any violations exist.

Problem 3 — Summary section also skips 'Info'.
The summary loop (lines 800–807) counts only 'High', 'Medium', and 'Low' severities, reinforcing that 'Info' was never a real category in the scanning pipeline.

Semantic Gap with ComplianceReport Class

ComplianceReport in SecurityClasses.psm1 has distinct TotalDependencies and PinnedDependencies fields and a CalculateScore() method that computes a meaningful ratio. This design implies the report should reflect all scanned dependencies (passing and failing), not just failure counts. The function needs access to total dependency counts for the score to be useful.

Expected Behavior

  • TotalDependencies reflects the total number of dependencies scanned across all files (both compliant and non-compliant).
  • PinnedDependencies reflects the count of compliant (properly pinned) dependencies.
  • UnpinnedDependencies reflects the count of non-compliant dependencies (the violation list).
  • ComplianceScore computes a meaningful percentage: (PinnedDependencies / TotalDependencies) * 100.
  • Remove or repurpose the dead 'Info' severity filter since no code path produces it.

Actual Behavior

  • TotalDependencies = violation count (not total scanned dependencies).
  • PinnedDependencies = 0 (always, due to dead severity filter).
  • ComplianceScore = 0.0% whenever any violation exists, 100.0% otherwise.
  • The score provides no actionable signal about incremental compliance progress.

Suggested Fix

  1. Thread total dependency counts through the scanning pipeline so Get-ComplianceReportData receives both total scanned dependencies and violations.
  2. Set fields correctly: TotalDependencies = total scanned, UnpinnedDependencies = violations count, PinnedDependencies = total − violations.
  3. Remove the dead $_.Severity -ne 'Info' filter — or, if 'Info' severity will be introduced later, add a [ValidateSet('High','Medium','Low','Info')] constraint to DependencyViolation.Severity in SecurityClasses.psm1 to make the contract explicit.
  4. Update tests in Test-DependencyPinning.Tests.ps1 to validate partial compliance scores (e.g., 3 violations out of 10 total → 70% compliance).

Affected Files

  • scripts/security/Test-DependencyPinning.ps1Get-ComplianceReportData function and upstream callers
  • scripts/security/Modules/SecurityClasses.psm1DependencyViolation.Severity (missing ValidateSet), ComplianceReport class
  • scripts/tests/security/Test-DependencyPinning.Tests.ps1 — needs partial compliance test coverage
  • scripts/tests/security/SecurityClasses.Tests.ps1 — existing partial compliance tests on CalculateScore() already pass; ensure alignment

Related

Metadata

Metadata

Labels

bugSomething isn't working

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions