Module: StillActive::SbomReader

Extended by:
SbomReader
Included in:
SbomReader
Defined in:
lib/still_active/sbom_reader.rb

Overview

Reads a CycloneDX SBOM (e.g. produced by Syft) into a clean, normalized dependency set -- the breadth input for non-Ruby ecosystems, where the maintenance lens runs over deps.dev/ecosyste.ms.

parse returns both the assessable dependencies AND the components it could NOT assess (a real dependency in an unsupported ecosystem, or a library with no PURL -- typically a git/path/local source). Surfacing those is the point: silently dropping them would let an audit report "all clear" while ignoring the deps that are often the riskiest. Genuine non-package noise (GitHub Actions pkg:github, opaque binaries pkg:generic, and file/application components) is excluded, not surfaced.

read keeps the simple "just the dependencies" shape. Never raises -- a missing/malformed SBOM or a bad PURL degrades to empty/skip.

Defined Under Namespace

Classes: Result

Constant Summary collapse

ECOSYSTEMS =

PURL type -> still_active ecosystem (also the deps.dev system, lowercased).

{
  "gem" => :rubygems,
  "npm" => :npm,
  "pypi" => :pypi,
  "cargo" => :cargo,
  "maven" => :maven,
  "golang" => :go,
  "nuget" => :nuget,
}.freeze
NOISE_TYPES =

PURL types that are not package dependencies: CI actions and opaque binaries. Excluded from both lists (not "unassessed deps").

["github", "generic"].freeze

Instance Method Summary collapse

Instance Method Details

#parse(path) ⇒ Object



45
46
47
48
49
# File 'lib/still_active/sbom_reader.rb', line 45

def parse(path)
  parse_string(File.read(path))
rescue SystemCallError, IOError
  Result.new(dependencies: [], unassessable: [])
end

#parse_string(body) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/still_active/sbom_reader.rb', line 51

def parse_string(body)
  doc = JSON.parse(body)
  components = doc.is_a?(Hash) ? doc["components"] : nil
  return Result.new(dependencies: [], unassessable: []) unless components.is_a?(Array)

  # Only trust the prod/dev split when the generator actually marks it
  # somewhere (scope, or a dev property). syft-style SBOMs mark nothing, so an
  # unmarked component is genuinely unknown -- never assume it's production.
  # Scoped to library components: scope/properties are legal on any component
  # type (application/file/...), and a dev signal on one of those is unrelated
  # to whether the dependency library components are marked.
  marks_dev = components.any? { |c| c.is_a?(Hash) && c["type"] == "library" && dev_signal?(c) }

  deps = []
  unassessable = []
  components.each do |component|
    kind, entry = classify(component)
    entry[:production] = !dev_signal?(component) if kind == :dependency && marks_dev
    deps << entry if kind == :dependency
    unassessable << entry if kind == :unassessable
  end
  # uniq collapses a package that a generator lists more than once (e.g. Syft's
  # per-location entries); `production` is derived from each component's own
  # signal, so duplicates of the same name+version dedup cleanly unless a
  # generator marks the copies inconsistently (malformed; not observed).
  Result.new(dependencies: deps.uniq, unassessable: unassessable.uniq)
rescue JSON::ParserError
  Result.new(dependencies: [], unassessable: [])
end

#read(path) ⇒ Object



42
# File 'lib/still_active/sbom_reader.rb', line 42

def read(path) = parse(path).dependencies

#read_string(body) ⇒ Object



43
# File 'lib/still_active/sbom_reader.rb', line 43

def read_string(body) = parse_string(body).dependencies