Skip to content
Merged
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
162 changes: 81 additions & 81 deletions Sources/SkipKit/SBOM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ import Foundation
/// (for the Android/Gradle dependency tree).
///
/// See: https://spdx.github.io/spdx-spec/v2.3/
public struct SBOMDocument: Codable, Hashable {
struct SBOMDocument: Codable, Hashable {
/// The SPDX version this document conforms to (e.g., `SPDX-2.3`).
public var spdxVersion: String?
var spdxVersion: String?
/// The SPDX identifier for this document (typically `SPDXRef-DOCUMENT`).
public var SPDXID: String?
var SPDXID: String?
/// The data license for the SPDX document content (typically `CC0-1.0`).
public var dataLicense: String?
var dataLicense: String?
/// A human-readable name for the document.
public var name: String?
var name: String?
/// A unique URI namespace identifying this document.
public var documentNamespace: String?
var documentNamespace: String?
/// Information about how and when the SBOM was created.
public var creationInfo: SBOMCreationInfo?
var creationInfo: SBOMCreationInfo?
/// The packages described by this SBOM.
public var packages: [SBOMPackage]
var packages: [SBOMPackage]
/// Relationships between SPDX elements (e.g., DEPENDS_ON).
public var relationships: [SBOMRelationship]?
var relationships: [SBOMRelationship]?
/// Custom (non-SPDX-listed) license definitions used by packages in this document.
public var hasExtractedLicensingInfos: [SBOMExtractedLicense]?
var hasExtractedLicensingInfos: [SBOMExtractedLicense]?

public init(
init(
spdxVersion: String? = nil,
SPDXID: String? = nil,
dataLicense: String? = nil,
Expand All @@ -56,65 +56,65 @@ public struct SBOMDocument: Codable, Hashable {
}

/// Metadata about how the SBOM was generated.
public struct SBOMCreationInfo: Codable, Hashable {
struct SBOMCreationInfo: Codable, Hashable {
/// ISO 8601 timestamp of when the document was created.
public var created: String?
var created: String?
/// The tools and/or organizations that created this document.
public var creators: [String]?
var creators: [String]?
/// The version of the SPDX license list used.
public var licenseListVersion: String?
var licenseListVersion: String?

public init(created: String? = nil, creators: [String]? = nil, licenseListVersion: String? = nil) {
init(created: String? = nil, creators: [String]? = nil, licenseListVersion: String? = nil) {
self.created = created
self.creators = creators
self.licenseListVersion = licenseListVersion
}
}

/// A single package (dependency) tracked in the SBOM.
public struct SBOMPackage: Codable, Hashable, Identifiable {
struct SBOMPackage: Codable, Hashable, Identifiable {
/// The SPDX identifier for this package.
public var SPDXID: String?
var SPDXID: String?
/// Human-readable package name.
public var name: String?
var name: String?
/// Version string for the package.
public var versionInfo: String?
var versionInfo: String?
/// Supplier (organization or person) that distributes the package.
public var supplier: String?
var supplier: String?
/// Originator (the party that originally created the package).
public var originator: String?
var originator: String?
/// URL or other locator describing where the package can be downloaded.
public var downloadLocation: String?
var downloadLocation: String?
/// Free-form description of the package.
public var description: String?
var description: String?
/// A short summary of the package.
public var summary: String?
var summary: String?
/// The package homepage.
public var homepage: String?
var homepage: String?
/// The license that the SBOM author concluded applies (after analysis).
public var licenseConcluded: String?
var licenseConcluded: String?
/// The license that the package author declared in the package.
public var licenseDeclared: String?
var licenseDeclared: String?
/// Additional comments about the license.
public var licenseComments: String?
var licenseComments: String?
/// Copyright notices declared by the package.
public var copyrightText: String?
var copyrightText: String?
/// What this package is for (e.g., `LIBRARY`, `APPLICATION`).
public var primaryPackagePurpose: String?
var primaryPackagePurpose: String?
/// Source-info string (often from the gradle plugin).
public var sourceInfo: String?
var sourceInfo: String?
/// Whether the file contents of the package were analyzed.
public var filesAnalyzed: Bool?
var filesAnalyzed: Bool?
/// Cryptographic checksums for the package archive.
public var checksums: [SBOMChecksum]?
var checksums: [SBOMChecksum]?
/// External references such as `purl` (package URL) and SwiftPM repository URLs.
public var externalRefs: [SBOMExternalRef]?
var externalRefs: [SBOMExternalRef]?
/// License information extracted from the package's files.
public var licenseInfoFromFiles: [String]?
var licenseInfoFromFiles: [String]?

public var id: String { SPDXID ?? (name ?? "") }
var id: String { SPDXID ?? (name ?? "") }

public init(
init(
SPDXID: String? = nil,
name: String? = nil,
versionInfo: String? = nil,
Expand Down Expand Up @@ -158,41 +158,41 @@ public struct SBOMPackage: Codable, Hashable, Identifiable {
}

/// A cryptographic checksum entry on a package.
public struct SBOMChecksum: Codable, Hashable {
struct SBOMChecksum: Codable, Hashable {
/// Hash algorithm name (e.g., `SHA1`, `SHA256`).
public var algorithm: String?
var algorithm: String?
/// The hex-encoded checksum value.
public var checksumValue: String?
var checksumValue: String?

public init(algorithm: String? = nil, checksumValue: String? = nil) {
init(algorithm: String? = nil, checksumValue: String? = nil) {
self.algorithm = algorithm
self.checksumValue = checksumValue
}
}

/// An external reference attached to a package, such as a Package URL (`purl`) or SwiftPM repository.
public struct SBOMExternalRef: Codable, Hashable {
struct SBOMExternalRef: Codable, Hashable {
/// Category (e.g., `PACKAGE-MANAGER`, `SECURITY`).
public var referenceCategory: String?
var referenceCategory: String?
/// The locator string (the meaning depends on `referenceType`).
public var referenceLocator: String?
var referenceLocator: String?
/// The type of reference (e.g., `purl`, `swiftpm`).
public var referenceType: String?
var referenceType: String?

public init(referenceCategory: String? = nil, referenceLocator: String? = nil, referenceType: String? = nil) {
init(referenceCategory: String? = nil, referenceLocator: String? = nil, referenceType: String? = nil) {
self.referenceCategory = referenceCategory
self.referenceLocator = referenceLocator
self.referenceType = referenceType
}
}

/// A relationship between two SPDX elements (e.g., `A DEPENDS_ON B`).
public struct SBOMRelationship: Codable, Hashable {
public var spdxElementId: String?
public var relatedSpdxElement: String?
public var relationshipType: String?
struct SBOMRelationship: Codable, Hashable {
var spdxElementId: String?
var relatedSpdxElement: String?
var relationshipType: String?

public init(spdxElementId: String? = nil, relatedSpdxElement: String? = nil, relationshipType: String? = nil) {
init(spdxElementId: String? = nil, relatedSpdxElement: String? = nil, relationshipType: String? = nil) {
self.spdxElementId = spdxElementId
self.relatedSpdxElement = relatedSpdxElement
self.relationshipType = relationshipType
Expand All @@ -201,17 +201,17 @@ public struct SBOMRelationship: Codable, Hashable {

/// A custom license definition for licenses that are not on the SPDX License List.
/// Identified by `LicenseRef-…` rather than a standard SPDX identifier.
public struct SBOMExtractedLicense: Codable, Hashable {
struct SBOMExtractedLicense: Codable, Hashable {
/// The `LicenseRef-…` identifier used to refer to this license.
public var licenseId: String?
var licenseId: String?
/// The full text of the license.
public var extractedText: String?
var extractedText: String?
/// A human-readable name for the license.
public var name: String?
var name: String?
/// URLs where the license text or more information can be found.
public var seeAlsos: [String]?
var seeAlsos: [String]?

public init(licenseId: String? = nil, extractedText: String? = nil, name: String? = nil, seeAlsos: [String]? = nil) {
init(licenseId: String? = nil, extractedText: String? = nil, name: String? = nil, seeAlsos: [String]? = nil) {
self.licenseId = licenseId
self.extractedText = extractedText
self.name = name
Expand All @@ -222,16 +222,16 @@ public struct SBOMExtractedLicense: Codable, Hashable {
// MARK: - Loading

/// The standard resource name (without extension) for the iOS/Darwin SPDX SBOM file.
public let sbomDarwinResourceName = "sbom-darwin-ios.spdx"
let sbomDarwinResourceName = "sbom-darwin-ios.spdx"
/// The standard resource name (without extension) for the Android/Linux SPDX SBOM file.
public let sbomLinuxAndroidResourceName = "sbom-linux-android.spdx"
let sbomLinuxAndroidResourceName = "sbom-linux-android.spdx"
/// The file extension for SPDX SBOM files.
public let sbomResourceExtension = "json"
let sbomResourceExtension = "json"

extension SBOMDocument {
/// The default resource name for the SBOM appropriate for the current platform
/// (`sbom-darwin-ios.spdx` on Apple platforms, `sbom-linux-android.spdx` on Android).
public static var defaultResourceName: String {
static var defaultResourceName: String {
#if os(Android)
return sbomLinuxAndroidResourceName
#else
Expand All @@ -247,29 +247,29 @@ extension SBOMDocument {
/// - Parameter bundle: The bundle containing the SBOM resource.
/// - Returns: The parsed `SBOMDocument`, or `nil` if no SBOM resource is present in the bundle.
/// - Throws: A decoding error if the resource exists but cannot be parsed as SPDX JSON.
public static func load(from bundle: Bundle) throws -> SBOMDocument? {
static func load(from bundle: Bundle) throws -> SBOMDocument? {
guard let url = bundle.url(forResource: defaultResourceName, withExtension: sbomResourceExtension) else {
return nil
}
return try load(from: url)
}

/// Loads and parses an SPDX JSON SBOM document from the given file URL.
public static func load(from url: URL) throws -> SBOMDocument {
static func load(from url: URL) throws -> SBOMDocument {
let data = try Data(contentsOf: url)
return try parse(data: data)
}

/// Parses an SPDX JSON SBOM document from raw `Data`.
public static func parse(data: Data) throws -> SBOMDocument {
static func parse(data: Data) throws -> SBOMDocument {
let decoder = JSONDecoder()
return try decoder.decode(SBOMDocument.self, from: data)
}

/// Returns the raw bytes of the SBOM resource for the current platform from the given
/// bundle, if present. Useful for sharing the file via the system share sheet without
/// re-encoding it.
public static func rawData(from bundle: Bundle) -> Data? {
static func rawData(from bundle: Bundle) -> Data? {
guard let url = bundle.url(forResource: defaultResourceName, withExtension: sbomResourceExtension) else {
return nil
}
Expand All @@ -278,12 +278,12 @@ extension SBOMDocument {

/// Returns the URL of the SBOM resource for the current platform from the given
/// bundle, if present.
public static func resourceURL(in bundle: Bundle) -> URL? {
static func resourceURL(in bundle: Bundle) -> URL? {
return bundle.url(forResource: defaultResourceName, withExtension: sbomResourceExtension)
}

/// Returns `true` if the given bundle contains an SBOM resource for the current platform.
public static func bundleContainsSBOM(_ bundle: Bundle) -> Bool {
static func bundleContainsSBOM(_ bundle: Bundle) -> Bool {
return resourceURL(in: bundle) != nil
}
}
Expand All @@ -293,7 +293,7 @@ extension SBOMDocument {
/// `primaryPackagePurpose == "APPLICATION"` or those that match the document name),
/// sorted alphabetically by name (case-insensitive). This is the "flat" list of every
/// dependency users typically want to see in a Bill of Materials view.
public var dependencyPackages: [SBOMPackage] {
var dependencyPackages: [SBOMPackage] {
let docName = self.name ?? ""
let filtered = packages.filter { pkg in
if pkg.primaryPackagePurpose == "APPLICATION" {
Expand All @@ -312,7 +312,7 @@ extension SBOMDocument {
/// The SPDX identifier of the root package described by this document, found via the
/// `SPDXRef-DOCUMENT DESCRIBES <root>` relationship. Falls back to the first package
/// with `primaryPackagePurpose == "APPLICATION"` if no `DESCRIBES` relationship exists.
public var rootPackageSPDXID: String? {
var rootPackageSPDXID: String? {
if let rels = relationships {
for rel in rels {
if rel.relationshipType == "DESCRIBES" && rel.spdxElementId == "SPDXRef-DOCUMENT" {
Expand All @@ -331,7 +331,7 @@ extension SBOMDocument {
}

/// Looks up a package by its SPDX identifier.
public func package(forSPDXID spdxId: String) -> SBOMPackage? {
func package(forSPDXID spdxId: String) -> SBOMPackage? {
for pkg in packages {
if pkg.SPDXID == spdxId {
return pkg
Expand All @@ -343,7 +343,7 @@ extension SBOMDocument {
/// Returns the packages directly depended on by the package with the given SPDX
/// identifier (i.e., `<spdxId> DEPENDS_ON X`), sorted alphabetically by name.
/// If the document has no relationships, returns an empty array.
public func directDependencies(ofSPDXID spdxId: String) -> [SBOMPackage] {
func directDependencies(ofSPDXID spdxId: String) -> [SBOMPackage] {
guard let rels = relationships else { return [] }
var result: [SBOMPackage] = []
for rel in rels {
Expand All @@ -358,7 +358,7 @@ extension SBOMDocument {
}

/// Returns the direct dependencies of the given package, sorted alphabetically by name.
public func directDependencies(of package: SBOMPackage) -> [SBOMPackage] {
func directDependencies(of package: SBOMPackage) -> [SBOMPackage] {
guard let id = package.SPDXID else { return [] }
return directDependencies(ofSPDXID: id)
}
Expand All @@ -369,7 +369,7 @@ extension SBOMDocument {
/// If the document does not contain a `DESCRIBES` relationship or any `DEPENDS_ON`
/// relationships from the root, this falls back to `dependencyPackages` so the
/// hierarchy view still has something useful to display.
public var topLevelPackages: [SBOMPackage] {
var topLevelPackages: [SBOMPackage] {
guard let rootId = rootPackageSPDXID else {
return dependencyPackages
}
Expand All @@ -381,7 +381,7 @@ extension SBOMDocument {
}

/// Looks up an extracted license by its `LicenseRef-…` identifier.
public func extractedLicense(forId licenseId: String) -> SBOMExtractedLicense? {
func extractedLicense(forId licenseId: String) -> SBOMExtractedLicense? {
guard let infos = hasExtractedLicensingInfos else { return nil }
for info in infos {
if info.licenseId == licenseId {
Expand Down Expand Up @@ -416,15 +416,15 @@ public enum SBOMDisplayMode: String, Hashable {
// MARK: - License helpers

/// Helpers for working with SPDX license identifiers.
public enum SPDXLicense {
enum SPDXLicense {
/// `NOASSERTION` is the SPDX sentinel meaning "no information was provided".
public static let noAssertion = "NOASSERTION"
static let noAssertion = "NOASSERTION"
/// `NONE` is the SPDX sentinel meaning "the field has explicitly no value".
public static let none = "NONE"
static let none = "NONE"

/// Returns `true` if the given license string is missing or one of the SPDX
/// "no information" sentinels (`NOASSERTION`, `NONE`).
public static func isUnknown(_ license: String?) -> Bool {
static func isUnknown(_ license: String?) -> Bool {
guard let license = license else { return true }
if license.isEmpty { return true }
if license == noAssertion { return true }
Expand All @@ -436,7 +436,7 @@ public enum SPDXLicense {
/// stripping any compound expressions like `WITH` clauses or `OR`/`AND` operators.
/// Returns `nil` if no usable identifier can be extracted, or if the identifier is
/// a `LicenseRef-…` (which is a custom license, not on the SPDX list).
public static func canonicalIdentifier(_ license: String?) -> String? {
static func canonicalIdentifier(_ license: String?) -> String? {
guard let raw = license else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if isUnknown(trimmed) { return nil }
Expand All @@ -463,7 +463,7 @@ public enum SPDXLicense {

/// Returns the URL on spdx.org/licenses/ for the given SPDX license identifier,
/// or `nil` if no canonical SPDX identifier could be extracted.
public static func licensePageURL(for license: String?) -> URL? {
static func licensePageURL(for license: String?) -> URL? {
guard let id = canonicalIdentifier(license) else { return nil }
return URL(string: "https://spdx.org/licenses/\(id).html")
}
Expand Down