From 89773c0a12690d9f89095d5fe2deccb2ffea6821 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 11 Jun 2026 09:40:42 -0400 Subject: [PATCH 001/168] fix: support CycloneDX 1.7 (#4967) Signed-off-by: Keith Zantow --- schema/cyclonedx/cyclonedx.json | 1744 ++++++++++--- schema/cyclonedx/cyclonedx.xsd | 2235 ++++++++++++++--- schema/cyclonedx/spdx.schema.json | 852 ++++++- schema/cyclonedx/spdx.xsd | 167 +- .../cyclonedxjson/testdata/identify/1.7.json | 59 + .../TestCycloneDxDirectoryEncoder.golden | 4 +- .../snapshot/TestCycloneDxImageEncoder.golden | 4 +- .../cyclonedxxml/testdata/identify/1.7.xml | 33 + .../TestCycloneDxDirectoryEncoder.golden | 2 +- .../snapshot/TestCycloneDxImageEncoder.golden | 2 +- syft/format/internal/cyclonedxutil/encoder.go | 2 - .../format/internal/cyclonedxutil/versions.go | 108 +- .../internal/cyclonedxutil/versions_test.go | 107 + 13 files changed, 4520 insertions(+), 799 deletions(-) create mode 100644 syft/format/cyclonedxjson/testdata/identify/1.7.json create mode 100644 syft/format/cyclonedxxml/testdata/identify/1.7.xml create mode 100644 syft/format/internal/cyclonedxutil/versions_test.go diff --git a/schema/cyclonedx/cyclonedx.json b/schema/cyclonedx/cyclonedx.json index d720cc795..fa94bbf1e 100644 --- a/schema/cyclonedx/cyclonedx.json +++ b/schema/cyclonedx/cyclonedx.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "$id": "http://cyclonedx.org/schema/bom-1.7.schema.json", "type": "object", "title": "CycloneDX Bill of Materials Standard", "$comment" : "CycloneDX JSON schema is published under the terms of the Apache License 2.0.", @@ -16,7 +16,7 @@ "bomFormat": { "type": "string", "title": "BOM Format", - "description": "Specifies the format of the BOM. This helps to identify the file as CycloneDX since BOMs do not have a filename convention, nor does JSON schema support namespaces. This value MUST be \"CycloneDX\".", + "description": "Specifies the format of the BOM. This helps to identify the file as CycloneDX since BOMs do not have a filename convention, nor does JSON schema support namespaces. This value must be \"CycloneDX\".", "enum": [ "CycloneDX" ] @@ -25,12 +25,12 @@ "type": "string", "title": "CycloneDX Specification Version", "description": "The version of the CycloneDX specification the BOM conforms to.", - "examples": ["1.6"] + "examples": ["1.7"] }, "serialNumber": { "type": "string", "title": "BOM Serial Number", - "description": "Every BOM generated SHOULD have a unique serial number, even if the contents of the BOM have not changed over time. If specified, the serial number MUST conform to RFC-4122. Use of serial numbers is RECOMMENDED.", + "description": "Every BOM generated SHOULD have a unique serial number, even if the contents of the BOM have not changed over time. If specified, the serial number must conform to [RFC 4122](https://www.ietf.org/rfc/rfc4122.html). Use of serial numbers is recommended.", "examples": ["urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"], "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, @@ -100,7 +100,7 @@ "items": {"$ref": "#/definitions/formula"}, "uniqueItems": true, "title": "Formulation", - "description": "Describes how a component or service was manufactured or deployed. This is achieved through the use of formulas, workflows, tasks, and steps, which declare the precise steps to reproduce along with the observed formulas describing the steps which transpired in the manufacturing process." + "description": "Describes the formulation of any referencable object within the BOM, including components, services, metadata, declarations, or the BOM itself. This may encompass how the object was created, assembled, deployed, tested, certified, or otherwise brought into its present form. Common examples include software build pipelines, deployment processes, AI/ML model training, cryptographic key generation or certification, and third-party audits. Processes are modeled using declared and observed formulas, composed of workflows, tasks, and individual steps." }, "declarations": { "type": "object", @@ -121,7 +121,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." }, "thirdParty": { "type": "boolean", @@ -145,7 +145,7 @@ "title": "Attestation", "additionalProperties": false, "properties": { - "summary": { + "summary": { "type": "string", "title": "Summary", "description": "The short description explaining the main points of the attestation." @@ -178,7 +178,7 @@ "counterClaims": { "type": "array", "title": "Counter Claims", - "description": "The list of `bom-ref` to the counter claims being attested to.", + "description": "The list of `bom-ref` to the counter claims being attested to.", "items": { "$ref": "#/definitions/refLinkType" } }, "conformance": { @@ -202,7 +202,7 @@ "mitigationStrategies": { "type": "array", "title": "Mitigation Strategies", - "description": "The list of `bom-ref` to the evidence provided describing the mitigation strategies.", + "description": "The list of `bom-ref` to the evidence provided describing the mitigation strategies.", "items": { "$ref": "#/definitions/refLinkType" } } } @@ -250,12 +250,12 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." }, "target": { "$ref": "#/definitions/refLinkType", "title": "Target", - "description": "The `bom-ref` to a target representing a specific system, application, API, module, team, person, process, business unit, company, etc... that this claim is being applied to." + "description": "The `bom-ref` to a target representing a specific system, application, API, module, team, person, process, business unit, company, etc... that this claim is being applied to." }, "predicate": { "type": "string", @@ -265,7 +265,7 @@ "mitigationStrategies": { "type": "array", "title": "Mitigation Strategies", - "description": "The list of `bom-ref` to the evidence provided describing the mitigation strategies. Each mitigation strategy should include an explanation of how any weaknesses in the evidence will be mitigated.", + "description": "The list of `bom-ref` to the evidence provided describing the mitigation strategies. Each mitigation strategy should include an explanation of how any weaknesses in the evidence will be mitigated.", "items": { "$ref": "#/definitions/refLinkType" } }, "reasoning": { @@ -311,7 +311,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." }, "propertyName": { "type": "string", @@ -345,7 +345,7 @@ "properties": { "attachment": { "title": "Data Attachment", - "description": "An optional way to include textual or encoded data.", + "description": "A way to include textual or encoded data.", "$ref": "#/definitions/attachment" }, "url": { @@ -384,7 +384,7 @@ "type": "string", "format": "date-time", "title": "Expires", - "description": "The optional date and time (timestamp) when the evidence is no longer valid." + "description": "The date and time (timestamp) when the evidence is no longer valid." }, "author": { "$ref": "#/definitions/organizationalContact", @@ -433,6 +433,7 @@ "affirmation": { "type": "object", "title": "Affirmation", + "description": "A concise statement affirmed by an individual regarding all declarations, often used for third-party auditor acceptance or recipient acknowledgment. It includes a list of authorized signatories who assert the validity of the document on behalf of the organization.", "additionalProperties": false, "properties": { "statement": { @@ -513,13 +514,31 @@ "items": { "$ref": "#/definitions/standard" } + }, + "patents": { + "type": "array", + "title": "Patents", + "description": "The list of either individual patents or patent families.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/patent" }, + { "$ref": "#/definitions/patentFamily" } + ] + } } } }, + "citations": { + "type": "array", + "items": {"$ref": "#/definitions/citation"}, + "uniqueItems": true, + "title": "Citations", + "description": "A collection of attributions indicating which entity supplied information for specific fields within the BOM." + }, "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -532,12 +551,14 @@ }, "definitions": { "refType": { + "title": "BOM Reference", "description": "Identifier for referable and therefore interlinkable elements.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "type": "string", "minLength": 1, - "$comment": "TODO (breaking change): add a format constraint that prevents the value from staring with 'urn:cdx:'" + "$comment": "TODO (breaking change): add a format constraint that prevents the value from starting with 'urn:cdx:'" }, "refLinkType": { + "title": "BOM Reference", "description": "Descriptor for an element identified by the attribute 'bom-ref' in the same BOM document.\nIn contrast to `bomLinkElementType`.", "$ref": "#/definitions/refType" }, @@ -639,7 +660,7 @@ } ] } - }, + }, "tools": { "title": "Tools", "description": "The tool(s) used in the creation, enrichment, and validation of the BOM.", @@ -669,7 +690,8 @@ { "type": "array", "title": "Tools (legacy)", - "description": "[Deprecated] The tool(s) used in the creation, enrichment, and validation of the BOM.", + "description": "[Deprecated]\nThe tool(s) used in the creation, enrichment, and validation of the BOM.", + "deprecated": true, "items": {"$ref": "#/definitions/tool"} } ] @@ -709,16 +731,49 @@ "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": {"$ref": "#/definitions/property"} + }, + "distributionConstraints": { + "title": "Distribution Constraints", + "description": "Conditions and constraints governing the sharing and distribution of the data or components described by this BOM.", + "type": "object", + "properties": { + "tlp": { + "$ref": "#/definitions/tlpClassification", + "description": "The Traffic Light Protocol (TLP) classification that controls the sharing and distribution of the data that the BOM describes." + } + }, + "additionalProperties": false } } }, + "tlpClassification": { + "title": "Traffic Light Protocol (TLP) Classification", + "description": "Traffic Light Protocol (TLP) is a classification system for identifying the potential risk associated with artefact, including whether it is subject to certain types of legal, financial, or technical threats. Refer to [https://www.first.org/tlp/](https://www.first.org/tlp/) for further information.\nThe default classification is \"CLEAR\"", + "type" : "string", + "default": "CLEAR", + "enum": [ + "CLEAR", + "GREEN", + "AMBER", + "AMBER_AND_STRICT", + "RED" + ], + "meta:enum": { + "CLEAR": "The information is not subject to any restrictions as regards the sharing.", + "GREEN": "The information is subject to limited disclosure, and recipients can share it within their community but not via publicly accessible channels.", + "AMBER": "The information is subject to limited disclosure, and recipients can only share it on a need-to-know basis within their organization and with clients.", + "AMBER_AND_STRICT": "The information is subject to limited disclosure, and recipients can only share it on a need-to-know basis within their organization.", + "RED": "The information is subject to restricted distribution to individual recipients only and must not be shared." + } + }, "tool": { "type": "object", "title": "Tool", - "description": "[Deprecated] This will be removed in a future version. Use component or service instead. Information about the automated or manual tool used", + "description": "[Deprecated] This will be removed in a future version. Use component or service instead.\nInformation about the automated or manual tool used", "additionalProperties": false, + "deprecated": true, "properties": { "vendor": { "type": "string", @@ -751,14 +806,12 @@ }, "organizationalEntity": { "type": "object", - "title": "Organizational Entity", - "description": "", "additionalProperties": false, "properties": { "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "name": { "type": "string", @@ -793,14 +846,13 @@ }, "organizationalContact": { "type": "object", - "title": "Organizational Contact", - "description": "", "additionalProperties": false, + "title": "Organizational Person", "properties": { "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "name": { "type": "string", @@ -852,9 +904,9 @@ "meta:enum": { "application": "A software application. Refer to [https://en.wikipedia.org/wiki/Application_software](https://en.wikipedia.org/wiki/Application_software) for information about applications.", "framework": "A software framework. Refer to [https://en.wikipedia.org/wiki/Software_framework](https://en.wikipedia.org/wiki/Software_framework) for information on how frameworks vary slightly from libraries.", - "library": "A software library. Refer to [https://en.wikipedia.org/wiki/Library_(computing)](https://en.wikipedia.org/wiki/Library_(computing)) for information about libraries. All third-party and open source reusable components will likely be a library. If the library also has key features of a framework, then it should be classified as a framework. If not, or is unknown, then specifying library is RECOMMENDED.", + "library": "A software library. Refer to [https://en.wikipedia.org/wiki/Library_(computing)](https://en.wikipedia.org/wiki/Library_(computing)) for information about libraries. All third-party and open source reusable components will likely be a library. If the library also has key features of a framework, then it should be classified as a framework. If not, or is unknown, then specifying library is recommended.", "container": "A packaging and/or runtime format, not specific to any particular technology, which isolates software inside the container from software outside of a container through virtualization technology. Refer to [https://en.wikipedia.org/wiki/OS-level_virtualization](https://en.wikipedia.org/wiki/OS-level_virtualization).", - "platform": "A runtime environment which interprets or executes software. This may include runtimes such as those that execute bytecode or low-code/no-code application platforms.", + "platform": "A runtime environment that interprets or executes software. This may include runtimes such as those that execute bytecode, just-in-time compilers, interpreters, or low-code/no-code application platforms.", "operating-system": "A software operating system without regard to deployment model (i.e. installed on physical hardware, virtual machine, image, etc) Refer to [https://en.wikipedia.org/wiki/Operating_system](https://en.wikipedia.org/wiki/Operating_system).", "device": "A hardware device such as a processor or chip-set. A hardware device containing firmware SHOULD include a component for the physical hardware itself and another component of type 'firmware' or 'operating-system' (whichever is relevant), describing information about the software running on the device. See also the list of [known device properties](https://github.com/CycloneDX/cyclonedx-property-taxonomy/blob/main/cdx/device.md).", "device-driver": "A special type of software that operates or controls a particular type of device. Refer to [https://en.wikipedia.org/wiki/Device_driver](https://en.wikipedia.org/wiki/Device_driver).", @@ -871,14 +923,14 @@ "mime-type": { "type": "string", "title": "Mime-Type", - "description": "The optional mime-type of the component. When used on file components, the mime-type can provide additional context about the kind of file being represented, such as an image, font, or executable. Some library or framework components may also have an associated mime-type.", + "description": "The mime-type of the component. When used on file components, the mime-type can provide additional context about the kind of file being represented, such as an image, font, or executable. Some library or framework components may also have an associated mime-type.", "examples": ["image/jpeg"], "pattern": "^[-+a-z0-9.]+/[-+a-z0-9.]+$" }, "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the component elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "supplier": { "title": "Component Supplier", @@ -924,7 +976,18 @@ "version": { "$ref": "#/definitions/version", "title": "Component Version", - "description": "The component version. The version should ideally comply with semantic versioning but is not enforced." + "description": "The component version. The version should ideally comply with semantic versioning but is not enforced.\nMust be used exclusively, either 'version' or 'versionRange', but not both." + }, + "versionRange": { + "$ref": "#/definitions/versionRange", + "title": "Component Version Range", + "description": "For an external component, this specifies the accepted version range.\nThe value must adhere to the Package URL Version Range syntax (vers), as defined at https://github.com/package-url/vers-spec\nMay only be used if `.isExternal` is set to `true`.\nMust be used exclusively, either 'version' or 'versionRange', but not both." + }, + "isExternal": { + "type": "boolean", + "title": "Component Is External", + "description": "Determine whether this component is external.\nAn external component is one that is not part of an assembly, but is expected to be provided by the environment, regardless of the component's `.scope`. This setting can be useful for distinguishing which components are bundled with the product and which can be relied upon to be present in the deployment environment.\nThis may be set to `true` for runtime components only. For `$.metadata.component`, it must be set to `false`.", + "default": false }, "description": { "type": "string", @@ -963,6 +1026,10 @@ "description": "A copyright notice informing users of the underlying claims to copyright ownership in a published work.", "examples": ["Acme Inc"] }, + "patentAssertions": { + "$ref": "#/definitions/patentAssertions", + "title": "Component Patent(s)" + }, "cpe": { "type": "string", "title": "Common Platform Enumeration (CPE)", @@ -972,13 +1039,13 @@ "purl": { "type": "string", "title": "Package URL (purl)", - "description": "Asserts the identity of the component using package-url (purl). The purl, if specified, MUST be valid and conform to the specification defined at: [https://github.com/package-url/purl-spec](https://github.com/package-url/purl-spec). Refer to `@.evidence.identity` to optionally provide evidence that substantiates the assertion of the component's identity.", + "description": "Asserts the identity of the component using package-url (purl). The purl, if specified, must be valid and conform to the specification defined at: [https://github.com/package-url/purl-spec](https://github.com/package-url/purl-spec). Refer to `@.evidence.identity` to optionally provide evidence that substantiates the assertion of the component's identity.", "examples": ["pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar"] }, "omniborId": { "type": "array", "title": "OmniBOR Artifact Identifier (gitoid)", - "description": "Asserts the identity of the component using the OmniBOR Artifact ID. The OmniBOR, if specified, MUST be valid and conform to the specification defined at: [https://www.iana.org/assignments/uri-schemes/prov/gitoid](https://www.iana.org/assignments/uri-schemes/prov/gitoid). Refer to `@.evidence.identity` to optionally provide evidence that substantiates the assertion of the component's identity.", + "description": "Asserts the identity of the component using the OmniBOR Artifact ID. The OmniBOR, if specified, must be valid and conform to the specification defined at: [https://www.iana.org/assignments/uri-schemes/prov/gitoid](https://www.iana.org/assignments/uri-schemes/prov/gitoid). Refer to `@.evidence.identity` to optionally provide evidence that substantiates the assertion of the component's identity.", "items": { "type": "string" }, "examples": [ "gitoid:blob:sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", @@ -987,8 +1054,8 @@ }, "swhid": { "type": "array", - "title": "SoftWare Heritage Identifier", - "description": "Asserts the identity of the component using the Software Heritage persistent identifier (SWHID). The SWHID, if specified, MUST be valid and conform to the specification defined at: [https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html](https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html). Refer to `@.evidence.identity` to optionally provide evidence that substantiates the assertion of the component's identity.", + "title": "Software Heritage Identifier", + "description": "Asserts the identity of the component using the Software Heritage persistent identifier (SWHID). The SWHID, if specified, must be valid and conform to the specification defined at: [https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html](https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html). Refer to `@.evidence.identity` to optionally provide evidence that substantiates the assertion of the component's identity.", "items": { "type": "string" }, "examples": ["swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2"] }, @@ -1000,7 +1067,8 @@ "modified": { "type": "boolean", "title": "Component Modified From Original", - "description": "[Deprecated] This will be removed in a future version. Use the pedigree element instead to supply information on exactly how the component was modified. A boolean value indicating if the component has been modified from the original. A value of true indicates the component is a derivative of the original. A value of false indicates the component has not been modified from the original." + "description": "[Deprecated] This will be removed in a future version. Use the pedigree element instead to supply information on exactly how the component was modified.\nA boolean value indicating if the component has been modified from the original. A value of true indicates the component is a derivative of the original. A value of false indicates the component has not been modified from the original.", + "deprecated": true }, "pedigree": { "type": "object", @@ -1011,7 +1079,7 @@ "ancestors": { "type": "array", "title": "Ancestors", - "description": "Describes zero or more components in which a component is derived from. This is commonly used to describe forks from existing projects where the forked version contains a ancestor node containing the original component it was forked from. For example, Component A is the original component. Component B is the component being used and documented in the BOM. However, Component B contains a pedigree node with a single ancestor documenting Component A - the original component from which Component B is derived from.", + "description": "Describes zero or more components in which a component is derived from. This is commonly used to describe forks from existing projects where the forked version contains an ancestor node containing the original component it was forked from. For example, Component A is the original component. Component B is the component being used and documented in the BOM. However, Component B contains a pedigree node with a single ancestor documenting Component A - the original component from which Component B is derived from.", "items": {"$ref": "#/definitions/component"} }, "descendants": { @@ -1035,7 +1103,7 @@ "patches": { "type": "array", "title": "Patches", - "description": ">A list of zero or more patches describing how the component deviates from an ancestor, descendant, or variant. Patches may be complementary to commits or may be used in place of commits.", + "description": "A list of zero or more patches describing how the component deviates from an ancestor, descendant, or variant. Patches may be complementary to commits or may be used in place of commits.", "items": {"$ref": "#/definitions/patch"} }, "notes": { @@ -1066,9 +1134,9 @@ "releaseNotes": { "$ref": "#/definitions/releaseNotes", "title": "Release notes", - "description": "Specifies optional release notes." + "description": "Specifies release notes." }, - "modelCard": { + "modelCard": { "$ref": "#/definitions/modelCard", "title": "AI/ML Model Card" }, @@ -1076,7 +1144,7 @@ "type": "array", "items": {"$ref": "#/definitions/componentData"}, "title": "Data", - "description": "This object SHOULD be specified for any component of type `data` and MUST NOT be specified for other component types." + "description": "This object SHOULD be specified for any component of type `data` and must not be specified for other component types." }, "cryptoProperties": { "$ref": "#/definitions/cryptoProperties", @@ -1085,7 +1153,7 @@ "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": {"$ref": "#/definitions/property"} }, "tags": { @@ -1097,7 +1165,25 @@ "title": "Signature", "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)." } - } + }, + "allOf": [ + { + "description": "Requirement: ensure that `version` and `versionRange` are not present simultaneously.", + "not": { + "required": ["version", "versionRange"] + } + }, + { + "description": "Requirement: 'versionRange' must not be present when 'isExternal' is `false`.", + "if": { + "properties": { "isExternal": { "const": false } } + }, + "then": { + "not": { "required": ["versionRange"] } + }, + "else": true + } + ] }, "swid": { "type": "object", @@ -1162,13 +1248,18 @@ "contentType": { "type": "string", "title": "Content-Type", - "description": "Specifies the content type of the text. Defaults to text/plain if not specified.", - "default": "text/plain" + "description": "Specifies the format and nature of the data being attached, helping systems correctly interpret and process the content. Common content type examples include `application/json` for JSON data and `text/plain` for plain text documents.\n [RFC 2045 section 5.1](https://www.ietf.org/rfc/rfc2045.html#section-5.1) outlines the structure and use of content types. For a comprehensive list of registered content types, refer to the [IANA media types registry](https://www.iana.org/assignments/media-types/media-types.xhtml).", + "default": "text/plain", + "examples": [ + "text/plain", + "application/json", + "image/png" + ] }, "encoding": { "type": "string", "title": "Encoding", - "description": "Specifies the optional encoding the text is represented in.", + "description": "Specifies the encoding the text is represented in.", "enum": [ "base64" ], @@ -1216,7 +1307,9 @@ "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", - "BLAKE3" + "BLAKE3", + "Streebog-256", + "Streebog-512" ] }, "hash-content": { @@ -1226,9 +1319,165 @@ "examples": ["3942447fac867ae5cdb3229b658f4d48"], "pattern": "^([a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128})$" }, + "licensing": { + "type": "object", + "title": "Licensing information", + "description": "Licensing details describing the licensor/licensee, license type, renewal and expiration dates, and other important metadata", + "additionalProperties": false, + "properties": { + "altIds": { + "type": "array", + "title": "Alternate License Identifiers", + "description": "License identifiers that may be used to manage licenses and their lifecycle", + "items": { + "type": "string" + } + }, + "licensor": { + "title": "Licensor", + "description": "The individual or organization that grants a license to another individual or organization", + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Licensor (Organization)", + "description": "The organization that granted the license", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "title": "Licensor (Individual)", + "description": "The individual, not associated with an organization, that granted the license", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["individual"] + } + ] + }, + "licensee": { + "title": "Licensee", + "description": "The individual or organization for which a license was granted to", + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Licensee (Organization)", + "description": "The organization that was granted the license", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "title": "Licensee (Individual)", + "description": "The individual, not associated with an organization, that was granted the license", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["individual"] + } + ] + }, + "purchaser": { + "title": "Purchaser", + "description": "The individual or organization that purchased the license", + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Purchaser (Organization)", + "description": "The organization that purchased the license", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "title": "Purchaser (Individual)", + "description": "The individual, not associated with an organization, that purchased the license", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["individual"] + } + ] + }, + "purchaseOrder": { + "type": "string", + "title": "Purchase Order", + "description": "The purchase order identifier the purchaser sent to a supplier or vendor to authorize a purchase" + }, + "licenseTypes": { + "type": "array", + "title": "License Type", + "description": "The type of license(s) that was granted to the licensee.", + "items": { + "type": "string", + "enum": [ + "academic", + "appliance", + "client-access", + "concurrent-user", + "core-points", + "custom-metric", + "device", + "evaluation", + "named-user", + "node-locked", + "oem", + "perpetual", + "processor-points", + "subscription", + "user", + "other" + ], + "meta:enum": { + "academic": "A license that grants use of software solely for the purpose of education or research.", + "appliance": "A license covering use of software embedded in a specific piece of hardware.", + "client-access": "A Client Access License (CAL) allows client computers to access services provided by server software.", + "concurrent-user": "A Concurrent User license (aka floating license) limits the number of licenses for a software application and licenses are shared among a larger number of users.", + "core-points": "A license where the core of a computer's processor is assigned a specific number of points.", + "custom-metric": "A license for which consumption is measured by non-standard metrics.", + "device": "A license that covers a defined number of installations on computers and other types of devices.", + "evaluation": "A license that grants permission to install and use software for trial purposes.", + "named-user": "A license that grants access to the software to one or more pre-defined users.", + "node-locked": "A license that grants access to the software on one or more pre-defined computers or devices.", + "oem": "An Original Equipment Manufacturer license that is delivered with hardware, cannot be transferred to other hardware, and is valid for the life of the hardware.", + "perpetual": "A license where the software is sold on a one-time basis and the licensee can use a copy of the software indefinitely.", + "processor-points": "A license where each installation consumes points per processor.", + "subscription": "A license where the licensee pays a fee to use the software or service.", + "user": "A license that grants access to the software or service by a specified number of users.", + "other": "Another license type." + } + } + }, + "lastRenewal": { + "type": "string", + "format": "date-time", + "title": "Last Renewal", + "description": "The timestamp indicating when the license was last renewed. For new purchases, this is often the purchase or acquisition date. For non-perpetual licenses or subscriptions, this is the timestamp of when the license was last renewed." + }, + "expiration": { + "type": "string", + "format": "date-time", + "title": "Expiration", + "description": "The timestamp indicating when the current license expires (if applicable)." + } + } + }, "license": { "type": "object", "title": "License", + "description": "Specifies the details and attributes related to a software license. It can either include a valid SPDX license identifier or a named license, along with additional properties such as license acknowledgment, comprehensive commercial licensing information, and the full text of the license.", "oneOf": [ { "required": ["id"] @@ -1242,18 +1491,18 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the license elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the license elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "id": { "$ref": "spdx.schema.json", "title": "License ID (SPDX)", - "description": "A valid SPDX license ID", + "description": "A valid SPDX license identifier. If specified, this value must be one of the enumeration of valid SPDX license identifiers defined in the spdx.schema.json (or spdx.xml) subschema which is synchronized with the official SPDX license list.", "examples": ["Apache-2.0"] }, "name": { "type": "string", "title": "License Name", - "description": "If SPDX does not define the license used, this field may be used to provide the license name", + "description": "The name of the license. This may include the name of a commercial or proprietary license or an open source license that may not be defined by SPDX.", "examples": ["Acme Software License"] }, "acknowledgement": { @@ -1261,7 +1510,7 @@ }, "text": { "title": "License text", - "description": "An optional way to include the textual content of a license.", + "description": "A way to include the textual content of a license.", "$ref": "#/definitions/attachment" }, "url": { @@ -1271,165 +1520,11 @@ "examples": ["https://www.apache.org/licenses/LICENSE-2.0.txt"], "format": "iri-reference" }, - "licensing": { - "type": "object", - "title": "Licensing information", - "description": "Licensing details describing the licensor/licensee, license type, renewal and expiration dates, and other important metadata", - "additionalProperties": false, - "properties": { - "altIds": { - "type": "array", - "title": "Alternate License Identifiers", - "description": "License identifiers that may be used to manage licenses and their lifecycle", - "items": { - "type": "string" - } - }, - "licensor": { - "title": "Licensor", - "description": "The individual or organization that grants a license to another individual or organization", - "type": "object", - "additionalProperties": false, - "properties": { - "organization": { - "title": "Licensor (Organization)", - "description": "The organization that granted the license", - "$ref": "#/definitions/organizationalEntity" - }, - "individual": { - "title": "Licensor (Individual)", - "description": "The individual, not associated with an organization, that granted the license", - "$ref": "#/definitions/organizationalContact" - } - }, - "oneOf":[ - { - "required": ["organization"] - }, - { - "required": ["individual"] - } - ] - }, - "licensee": { - "title": "Licensee", - "description": "The individual or organization for which a license was granted to", - "type": "object", - "additionalProperties": false, - "properties": { - "organization": { - "title": "Licensee (Organization)", - "description": "The organization that was granted the license", - "$ref": "#/definitions/organizationalEntity" - }, - "individual": { - "title": "Licensee (Individual)", - "description": "The individual, not associated with an organization, that was granted the license", - "$ref": "#/definitions/organizationalContact" - } - }, - "oneOf":[ - { - "required": ["organization"] - }, - { - "required": ["individual"] - } - ] - }, - "purchaser": { - "title": "Purchaser", - "description": "The individual or organization that purchased the license", - "type": "object", - "additionalProperties": false, - "properties": { - "organization": { - "title": "Purchaser (Organization)", - "description": "The organization that purchased the license", - "$ref": "#/definitions/organizationalEntity" - }, - "individual": { - "title": "Purchaser (Individual)", - "description": "The individual, not associated with an organization, that purchased the license", - "$ref": "#/definitions/organizationalContact" - } - }, - "oneOf":[ - { - "required": ["organization"] - }, - { - "required": ["individual"] - } - ] - }, - "purchaseOrder": { - "type": "string", - "title": "Purchase Order", - "description": "The purchase order identifier the purchaser sent to a supplier or vendor to authorize a purchase" - }, - "licenseTypes": { - "type": "array", - "title": "License Type", - "description": "The type of license(s) that was granted to the licensee.", - "items": { - "type": "string", - "enum": [ - "academic", - "appliance", - "client-access", - "concurrent-user", - "core-points", - "custom-metric", - "device", - "evaluation", - "named-user", - "node-locked", - "oem", - "perpetual", - "processor-points", - "subscription", - "user", - "other" - ], - "meta:enum": { - "academic": "A license that grants use of software solely for the purpose of education or research.", - "appliance": "A license covering use of software embedded in a specific piece of hardware.", - "client-access": "A Client Access License (CAL) allows client computers to access services provided by server software.", - "concurrent-user": "A Concurrent User license (aka floating license) limits the number of licenses for a software application and licenses are shared among a larger number of users.", - "core-points": "A license where the core of a computer's processor is assigned a specific number of points.", - "custom-metric": "A license for which consumption is measured by non-standard metrics.", - "device": "A license that covers a defined number of installations on computers and other types of devices.", - "evaluation": "A license that grants permission to install and use software for trial purposes.", - "named-user": "A license that grants access to the software to one or more pre-defined users.", - "node-locked": "A license that grants access to the software on one or more pre-defined computers or devices.", - "oem": "An Original Equipment Manufacturer license that is delivered with hardware, cannot be transferred to other hardware, and is valid for the life of the hardware.", - "perpetual": "A license where the software is sold on a one-time basis and the licensee can use a copy of the software indefinitely.", - "processor-points": "A license where each installation consumes points per processor.", - "subscription": "A license where the licensee pays a fee to use the software or service.", - "user": "A license that grants access to the software or service by a specified number of users.", - "other": "Another license type." - } - } - }, - "lastRenewal": { - "type": "string", - "format": "date-time", - "title": "Last Renewal", - "description": "The timestamp indicating when the license was last renewed. For new purchases, this is often the purchase or acquisition date. For non-perpetual licenses or subscriptions, this is the timestamp of when the license was last renewed." - }, - "expiration": { - "type": "string", - "format": "date-time", - "title": "Expiration", - "description": "The timestamp indicating when the current license expires (if applicable)." - } - } - }, + "licensing": {"$ref": "#/definitions/licensing"}, "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": {"$ref": "#/definitions/property"} } } @@ -1449,56 +1544,108 @@ }, "licenseChoice": { "title": "License Choice", - "description": "EITHER (list of SPDX licenses and/or named licenses) OR (tuple of one SPDX License Expression)", + "description": "A list of SPDX licenses and/or named licenses and/or SPDX License Expression.", "type": "array", - "oneOf": [ - { - "title": "Multiple licenses", - "description": "A list of SPDX licenses and/or named licenses.", - "type": "array", - "items": { + "items": { + "oneOf": [ + { "type": "object", "title": "License", - "required": ["license"], + "required": [ + "license" + ], "additionalProperties": false, "properties": { - "license": {"$ref": "#/definitions/license"} + "license": { + "$ref": "#/definitions/license" + } } - } - }, - { - "title": "SPDX License Expression", - "description": "A tuple of exactly one SPDX License Expression.", - "type": "array", - "additionalItems": false, - "minItems": 1, - "maxItems": 1, - "items": [{ + }, + { + "title": "License Expression", + "description": "Specifies the details and attributes related to a software license.\nIt must be a valid SPDX license expression, along with additional properties such as license acknowledgment.", "type": "object", "additionalProperties": false, - "required": ["expression"], + "required": [ + "expression" + ], "properties": { "expression": { "type": "string", "title": "SPDX License Expression", - "description": "A valid SPDX license expression.\nRefer to https://spdx.org/specifications for syntax requirements", + "description": "A valid SPDX license expression.\nRefer to https://spdx.org/specifications for syntax requirements.", "examples": [ "Apache-2.0 AND (MIT OR GPL-2.0-only)", "GPL-3.0-only WITH Classpath-exception-2.0" ] }, + "expressionDetails": { + "title": "Expression Details", + "description": "Details for parts of the `expression`.", + "type": "array", + "items": { + "type": "object", + "description": "This document specifies the details and attributes related to a software license identifier. An SPDX expression may be a compound of license identifiers.\nThe `license_identifier` property serves as the key that identifies each record. Note that this key is not required to be unique, as the same license identifier could apply to multiple, different but similar license details, texts, etc.", + "required": [ + "licenseIdentifier" + ], + "properties": { + "licenseIdentifier": { + "title": "License Identifier", + "description": "The valid SPDX license identifier. Refer to https://spdx.org/specifications for syntax requirements.\nThis property serves as the primary key, which uniquely identifies each record.", + "type": "string", + "examples": [ + "Apache-2.0", + "GPL-3.0-only WITH Classpath-exception-2.0", + "LicenseRef-my-custom-license" + ] + }, + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An identifier which can be used to reference the license elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + }, + "text": { + "title": "License texts", + "description": "A way to include the textual content of the license.", + "$ref": "#/definitions/attachment" + }, + "url": { + "type": "string", + "title": "License URL", + "description": "The URL to the license file. If specified, a 'license' externalReference should also be specified for completeness", + "examples": [ + "https://www.apache.org/licenses/LICENSE-2.0.txt" + ], + "format": "iri-reference" + } + }, + "additionalProperties": false + } + }, "acknowledgement": { "$ref": "#/definitions/licenseAcknowledgementEnumeration" }, "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the license elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the license elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + }, + "licensing": { + "$ref": "#/definitions/licensing" + }, + "properties": { + "type": "array", + "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", + "items": { + "$ref": "#/definitions/property" + } } } - }] - } - ] + } + ] + } }, "commit": { "type": "object", @@ -1581,7 +1728,7 @@ "properties": { "text": { "title": "Diff text", - "description": "Specifies the optional text of the diff", + "description": "Specifies the text of the diff", "$ref": "#/definitions/attachment" }, "url": { @@ -1720,7 +1867,7 @@ "comment": { "type": "string", "title": "Comment", - "description": "An optional comment describing the external reference" + "description": "A comment describing the external reference" }, "type": { "type": "string", @@ -1769,6 +1916,10 @@ "electronic-signature", "digital-signature", "rfc-9116", + "patent", + "patent-family", + "patent-assertion", + "citation", "other" ], "meta:enum": { @@ -1794,7 +1945,7 @@ "log": "A record of events that occurred in a computer system or application, such as problems, errors, or information on current operations.", "configuration": "Parameters or settings that may be used by other components or services.", "evidence": "Information used to substantiate a claim.", - "formulation": "Describes how a component or service was manufactured or deployed.", + "formulation": "Describes the formulation of any referencable object within the BOM, including components, services, metadata, declarations, or the BOM itself.", "attestation": "Human or machine-readable statements containing facts, evidence, or testimony.", "threat-model": "An enumeration of identified weaknesses, threats, and countermeasures, dataflow diagram (DFD), attack tree, and other supporting documentation in human-readable or machine-readable format.", "adversary-model": "The defined assumptions, goals, and capabilities of an adversary.", @@ -1810,10 +1961,14 @@ "certification-report": "Industry, regulatory, or other certification from an accredited (if applicable) certification body.", "codified-infrastructure": "Code or configuration that defines and provisions virtualized infrastructure, commonly referred to as Infrastructure as Code (IaC).", "quality-metrics": "Report or system in which quality metrics can be obtained.", - "poam": "Plans of Action and Milestones (POAM) complement an \"attestation\" external reference. POAM is defined by NIST as a \"document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones\".", + "poam": "Plans of Action and Milestones (POA&M) complement an \"attestation\" external reference. POA&M is defined by NIST as a \"document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones\".", "electronic-signature": "An e-signature is commonly a scanned representation of a written signature or a stylized script of the person's name.", "digital-signature": "A signature that leverages cryptography, typically public/private key pairs, which provides strong authenticity verification.", - "rfc-9116": "Document that complies with RFC-9116 (A File Format to Aid in Security Vulnerability Disclosure)", + "rfc-9116": "Document that complies with [RFC 9116](https://www.ietf.org/rfc/rfc9116.html) (A File Format to Aid in Security Vulnerability Disclosure)", + "patent": "References information about patents which may be defined in human-readable documents or in machine-readable formats such as CycloneDX or ST.96. For detailed patent information or to reference the information provided directly by patent offices, it is recommended to leverage standards from the World Intellectual Property Organization (WIPO) such as [ST.96](https://www.wipo.int/standards/en/st96).", + "patent-family": "References information about a patent family which may be defined in human-readable documents or in machine-readable formats such as CycloneDX or ST.96. A patent family is a group of related patent applications or granted patents that cover the same or similar invention. For detailed patent family information or to reference the information provided directly by patent offices, it is recommended to leverage standards from the World Intellectual Property Organization (WIPO) such as [ST.96](https://www.wipo.int/standards/en/st96).", + "patent-assertion" : "References assertions made regarding patents associated with a component or service. Assertions distinguish between ownership, licensing, and other relevant interactions with patents.", + "citation": "A reference to external citations applicable to the object identified by this BOM entry or the BOM itself. When used with a BOM-Link, this allows offloading citations into a separate CycloneDX BOM.", "other": "Use this if no other types accurately describe the purpose of the external reference." } }, @@ -1822,13 +1977,19 @@ "items": {"$ref": "#/definitions/hash"}, "title": "Hashes", "description": "The hashes of the external reference (if applicable)." + }, + "properties": { + "type": "array", + "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", + "items": {"$ref": "#/definitions/property"} } } }, "dependency": { "type": "object", "title": "Dependency", - "description": "Defines the direct dependencies of a component, service, or the components provided/implemented by a given component. Components or services that do not have their own dependencies MUST be declared as empty elements within the graph. Components or services that are not represented in the dependency graph MAY have unknown dependencies. It is RECOMMENDED that implementations assume this to be opaque and not an indicator of an object being dependency-free. It is RECOMMENDED to leverage compositions to indicate unknown dependency graphs.", + "description": "Defines the direct dependencies of a component, service, or the components provided/implemented by a given component. Components or services that do not have their own dependencies must be declared as empty elements within the graph. Components or services that are not represented in the dependency graph may have unknown dependencies. It is recommended that implementations assume this to be opaque and not an indicator of an object being dependency-free. It is recommended to leverage compositions to indicate unknown dependency graphs.", "required": [ "ref" ], @@ -1870,7 +2031,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the service elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the service elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "provider": { "title": "Provider", @@ -1932,7 +2093,11 @@ }, "licenses": { "$ref": "#/definitions/licenseChoice", - "title": "Component License(s)" + "title": "Service License(s)" + }, + "patentAssertions": { + "$ref": "#/definitions/patentAssertions", + "title": "Service Patent(s)" }, "externalReferences": { "type": "array", @@ -1950,12 +2115,12 @@ "releaseNotes": { "$ref": "#/definitions/releaseNotes", "title": "Release notes", - "description": "Specifies optional release notes." + "description": "Specifies release notes." }, "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": {"$ref": "#/definitions/property"} }, "tags": { @@ -2064,6 +2229,7 @@ "copyright": { "type": "object", "title": "Copyright", + "description": "A copyright notice informing users of the underlying claims to copyright ownership in a published work.", "required": [ "text" ], @@ -2071,7 +2237,8 @@ "properties": { "text": { "type": "string", - "title": "Copyright Text" + "title": "Copyright Text", + "description": "The textual content of the copyright." } } }, @@ -2083,7 +2250,7 @@ "properties": { "identity": { "title": "Identity Evidence", - "description": "Evidence that substantiates the identity of a component. The identity may be an object or an array of identity objects. Support for specifying identity as a single object was introduced in CycloneDX v1.5. Arrays were introduced in v1.6. It is RECOMMENDED that all implementations use arrays, even if only one identity object is specified.", + "description": "Evidence that substantiates the identity of a component. The identity may be an object or an array of identity objects. Support for specifying identity as a single object was introduced in CycloneDX v1.5. Arrays were introduced in v1.6. It is recommended that all implementations use arrays, even if only one identity object is specified.", "oneOf" : [ { "type": "array", @@ -2110,7 +2277,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the occurrence elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the occurrence elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "location": { "type": "string", @@ -2176,7 +2343,7 @@ }, "parameters": { "title": "Parameters", - "description": "Optional arguments that are passed to the module or function.", + "description": "Arguments that are passed to the module or function.", "type": "array", "items": { "type": "string" @@ -2225,12 +2392,12 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the composition elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the composition elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "aggregate": { "$ref": "#/definitions/aggregateType", "title": "Aggregate", - "description": "Specifies an aggregate type that describe how complete a relationship is." + "description": "Specifies an aggregate type that describes how complete a relationship is." }, "assemblies": { "type": "array", @@ -2306,10 +2473,11 @@ "property": { "type": "object", "title": "Lightweight name-value pair", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "required": [ "name" ], + "additionalProperties": false, "properties": { "name": { "type": "string", @@ -2321,14 +2489,13 @@ "title": "Value", "description": "The value of the property." } - }, - "additionalProperties": false + } }, "localeType": { "type": "string", "pattern": "^([a-z]{2})(-[A-Z]{2})?$", "title": "Locale", - "description": "Defines a syntax for representing two character language code (ISO-639) followed by an optional two character country code. The language code MUST be lower case. If the country code is specified, the country code MUST be upper case. The language code and country code MUST be separated by a minus sign. Examples: en, en-US, fr, fr-CA" + "description": "Defines a syntax for representing two character language code (ISO-639) followed by an optional two character country code. The language code must be lower case. If the country code is specified, the country code must be upper case. The language code and country code must be separated by a minus sign. Examples: en, en-US, fr, fr-CA" }, "releaseType": { "type": "string", @@ -2339,7 +2506,7 @@ "pre-release", "internal" ], - "description": "The software versioning type. It is RECOMMENDED that the release type use one of 'major', 'minor', 'patch', 'pre-release', or 'internal'. Representing all possible software release types is not practical, so standardizing on the recommended values, whenever possible, is strongly encouraged.\n\n* __major__ = A major release may contain significant changes or may introduce breaking changes.\n* __minor__ = A minor release, also known as an update, may contain a smaller number of changes than major releases.\n* __patch__ = Patch releases are typically unplanned and may resolve defects or important security issues.\n* __pre-release__ = A pre-release may include alpha, beta, or release candidates and typically have limited support. They provide the ability to preview a release prior to its general availability.\n* __internal__ = Internal releases are not for public consumption and are intended to be used exclusively by the project or manufacturer that produced it." + "description": "The software versioning type. It is recommended that the release type use one of 'major', 'minor', 'patch', 'pre-release', or 'internal'. Representing all possible software release types is not practical, so standardizing on the recommended values, whenever possible, is strongly encouraged.\n\n* __major__ = A major release may contain significant changes or may introduce breaking changes.\n* __minor__ = A minor release, also known as an update, may contain a smaller number of changes than major releases.\n* __patch__ = Patch releases are typically unplanned and may resolve defects or important security issues.\n* __pre-release__ = A pre-release may include alpha, beta, or release candidates and typically have limited support. They provide the ability to preview a release prior to its general availability.\n* __internal__ = Internal releases are not for public consumption and are intended to be used exclusively by the project or manufacturer that produced it." }, "note": { "type": "object", @@ -2430,7 +2597,7 @@ "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": {"$ref": "#/definitions/property"} } } @@ -2445,7 +2612,7 @@ "title": { "type": "string", "title": "Title", - "description": "An optional name of the advisory." + "description": "A name of the advisory." }, "url": { "type": "string", @@ -2585,7 +2752,7 @@ "justification": { "type": "string", "title": "Justification", - "description": "An optional reason for rating the vulnerability as it was" + "description": "A reason for rating the vulnerability as it was" } } }, @@ -2626,7 +2793,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the vulnerability elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the vulnerability elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "id": { "type": "string", @@ -2674,7 +2841,7 @@ "ratings": { "type": "array", "title": "Ratings", - "description": "List of vulnerability ratings", + "description": "List of vulnerability ratings. Consumers SHOULD consider ratings in prioritization decisions; source ratings may differ and aid prioritization.", "items": { "$ref": "#/definitions/rating" } @@ -2816,7 +2983,8 @@ { "type": "array", "title": "Tools (legacy)", - "description": "[Deprecated] The tool(s) used to identify, confirm, or score the vulnerability.", + "description": "[Deprecated]\nThe tool(s) used to identify, confirm, or score the vulnerability.", + "deprecated": true, "items": {"$ref": "#/definitions/tool"} } ] @@ -2921,7 +3089,7 @@ }, "range": { "title": "Version Range", - "description": "A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst", + "description": "A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/vers-spec", "$ref": "#/definitions/versionRange" }, "status": { @@ -2941,7 +3109,7 @@ "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -2976,7 +3144,7 @@ ] }, "versionRange": { - "description": "A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst", + "description": "A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/vers-spec", "type": "string", "minLength": 1, "maxLength": 4096, @@ -3008,7 +3176,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the annotation elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the annotation elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "subjects": { "type": "array", @@ -3096,13 +3264,13 @@ "$comment": "Model card support in CycloneDX is derived from TensorFlow Model Card Toolkit released under the Apache 2.0 license and available from https://github.com/tensorflow/model-card-toolkit/blob/main/model_card_toolkit/schema/v0.0.2/model_card.schema.json. In addition, CycloneDX model card support includes portions of VerifyML, also released under the Apache 2.0 license and available from https://github.com/cylynx/verifyml/blob/main/verifyml/model_card_toolkit/schema/v0.0.4/model_card.schema.json.", "type": "object", "title": "Model Card", - "description": "A model card describes the intended uses of a machine learning model and potential limitations, including biases and ethical considerations. Model cards typically contain the training parameters, which datasets were used to train the model, performance metrics, and other relevant data useful for ML transparency. This object SHOULD be specified for any component of type `machine-learning-model` and MUST NOT be specified for other component types.", + "description": "A model card describes the intended uses of a machine learning model and potential limitations, including biases and ethical considerations. Model cards typically contain the training parameters, which datasets were used to train the model, performance metrics, and other relevant data useful for ML transparency. This object SHOULD be specified for any component of type `machine-learning-model` and must not be specified for other component types.", "additionalProperties": false, "properties": { "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the model card elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the model card elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "modelParameters": { "type": "object", @@ -3179,6 +3347,7 @@ } ], "title": "Reference", + "type": "string", "description": "References a data component by the components bom-ref attribute" } } @@ -3277,7 +3446,7 @@ "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": {"$ref": "#/definitions/property"} } } @@ -3305,7 +3474,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the dataset elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." + "description": "An identifier which can be used to reference the dataset elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links." }, "type": { "type": "string", @@ -3339,7 +3508,7 @@ "properties": { "attachment": { "title": "Data Attachment", - "description": "An optional way to include textual or encoded data.", + "description": "A way to include textual or encoded data.", "$ref": "#/definitions/attachment" }, "url": { @@ -3413,10 +3582,12 @@ "properties": { "organization": { "title": "Organization", + "description": "The organization that is responsible for specific data governance role(s).", "$ref": "#/definitions/organizationalEntity" }, "contact": { "title": "Individual", + "description": "The individual that is responsible for specific data governance role(s).", "$ref": "#/definitions/organizationalContact" } }, @@ -3460,7 +3631,7 @@ }, "image": { "title": "Graphic Image", - "description": "The graphic (vector or raster). Base64 encoding MUST be specified for binary images.", + "description": "The graphic (vector or raster). Base64 encoding must be specified for binary images.", "$ref": "#/definitions/attachment" } } @@ -3572,6 +3743,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -3640,6 +3812,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -3711,7 +3884,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the energy provider elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the energy provider elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "description": { @@ -3722,6 +3895,7 @@ "organization": { "type": "object", "title": "Organization", + "description": "The organization that provides energy.", "$ref": "#/definitions/organizationalEntity" }, "energySource": { @@ -3776,7 +3950,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the address elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the address elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "country": { @@ -3824,7 +3998,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the formula elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the formula elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "components": { @@ -3858,6 +4032,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -3867,7 +4042,7 @@ "workflow": { "title": "Workflow", "description": "A specialized orchestration task.", - "$comment": "Workflow are as task themselves and can trigger other workflow tasks. These relationships can be modeled in the taskDependencies graph.", + "$comment": "Workflow are as task themselves and can trigger other workflow tasks. These relationships can be modeled in the taskDependencies graph.", "type": "object", "required": [ "bom-ref", @@ -3878,7 +4053,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the workflow elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the workflow elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "uid": { @@ -3991,7 +4166,7 @@ "runtimeTopology": { "title": "Runtime topology", "description": "A graph of the component runtime topology for workflow's instance.", - "$comment": "A description of the runtime component and service topology. This can describe a partial or complete topology used to host and execute the task (e.g., hardware, operating systems, configurations, etc.),", + "$comment": "A description of the runtime component and service topology. This can describe a partial or complete topology used to host and execute the task (e.g., hardware, operating systems, configurations, etc.),", "type": "array", "uniqueItems": true, "items": { @@ -4001,6 +4176,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4021,7 +4197,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the task elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the task elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "uid": { @@ -4115,7 +4291,7 @@ "runtimeTopology": { "title": "Runtime topology", "description": "A graph of the component runtime topology for task's instance.", - "$comment": "A description of the runtime component and service topology. This can describe a partial or complete topology used to host and execute the task (e.g., hardware, operating systems, configurations, etc.),", + "$comment": "A description of the runtime component and service topology. This can describe a partial or complete topology used to host and execute the task (e.g., hardware, operating systems, configurations, etc.),", "type": "array", "items": { "$ref": "#/definitions/dependency" @@ -4125,6 +4301,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4157,6 +4334,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4175,6 +4353,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4193,7 +4372,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the workspace elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the workspace elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "uid": { @@ -4266,6 +4445,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4321,6 +4501,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4340,7 +4521,7 @@ "properties": { "bom-ref": { "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the trigger elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", + "description": "An identifier which can be used to reference the trigger elsewhere in the BOM. Every `bom-ref` must be unique within the BOM.\nValue SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.", "$ref": "#/definitions/refType" }, "uid": { @@ -4386,6 +4567,7 @@ "conditions": { "type": "array", "title": "Conditions", + "description": "A list of conditions used to determine if a trigger should be activated.", "uniqueItems": true, "items": { "$ref": "#/definitions/condition" @@ -4420,6 +4602,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4466,6 +4649,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4547,7 +4731,9 @@ "$ref": "#/definitions/property" }, { - "type": "string" + "type": "string", + "title": "String-Based Environment Variables", + "description": "In addition to the more common key–value pair format, some environment variables may consist of a single string without an explicit value assignment. These string-based environment variables typically act as flags or signals to software, indicating that a feature should be enabled, a mode should be activated, or a specific condition is present. Their presence alone conveys meaning." } ] } @@ -4560,6 +4746,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4636,7 +4823,9 @@ "$ref": "#/definitions/property" }, { - "type": "string" + "type": "string", + "title": "String-Based Environment Variables", + "description": "In addition to the more common key–value pair format, some environment variables may consist of a single string without an explicit value assignment. These string-based environment variables typically act as flags or signals to software, indicating that a feature should be enabled, a mode should be activated, or a specific condition is present. Their presence alone conveys meaning." } ] }, @@ -4645,6 +4834,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4710,6 +4900,7 @@ "properties": { "type": "array", "title": "Properties", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4870,7 +5061,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." }, "name": { "type": "string", @@ -4904,7 +5095,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." }, "identifier": { "type": "string", @@ -4925,14 +5116,14 @@ "type": "array", "title": "Descriptions", "description": "The supplemental text that provides additional guidance or context to the requirement, but is not directly part of the requirement.", - "items": { "type": "string" } + "items": { "type": "string" } }, "openCre": { "type": "array", "title": "OWASP OpenCRE Identifier(s)", "description": "The Common Requirements Enumeration (CRE) identifier(s). CRE is a structured and standardized framework for uniting security standards and guidelines. CRE links each section of a resource to a shared topic identifier (a Common Requirement). Through this shared topic link, all resources map to each other. Use of CRE promotes clear and unambiguous communication among stakeholders.", "items": { - "type": "string", + "type": "string", "pattern": "^CRE:[0-9]+-[0-9]+$", "examples": [ "CRE:764-507" ] } @@ -4940,12 +5131,12 @@ "parent": { "$ref": "#/definitions/refLinkType", "title": "Parent BOM Reference", - "description": "The optional `bom-ref` to a parent requirement. This establishes a hierarchy of requirements. Top-level requirements must not define a parent. Only child requirements should define parents." + "description": "The `bom-ref` to a parent requirement. This establishes a hierarchy of requirements. Top-level requirements must not define a parent. Only child requirements should define parents." }, "properties": { "type": "array", "title": "Properties", - "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.", + "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is optional.", "items": { "$ref": "#/definitions/property" } @@ -4971,7 +5162,7 @@ "bom-ref": { "$ref": "#/definitions/refType", "title": "BOM Reference", - "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." }, "identifier": { "type": "string", @@ -5065,6 +5256,7 @@ "kem", "ae", "combiner", + "key-wrap", "other", "unknown" ], @@ -5082,19 +5274,32 @@ "kem": "A Key Encapsulation Mechanism (KEM) algorithm is a mechanism for transporting random keying material to a recipient using the recipient's public key.", "ae": "Authenticated Encryption (AE) is a cryptographic process that provides both confidentiality and data integrity. It ensures that the encrypted data has not been tampered with and comes from a legitimate source. AE is commonly used in secure communication protocols.", "combiner": "A combiner aggregates many candidates for a cryptographic primitive and generates a new candidate for the same primitive.", + "key-wrap": "Key-wrap is a cryptographic technique used to securely encrypt and protect cryptographic keys using algorithms like AES.", "other": "Another primitive type.", "unknown": "The primitive is not known." } }, + "algorithmFamily": { + "$ref": "cryptography-defs.schema.json#/definitions/algorithmFamiliesEnum", + "title": "Algorithm Family", + "description": "A valid algorithm family identifier. If specified, this value must be one of the enumeration of valid algorithm Family identifiers defined in the `cryptography-defs.schema.json` subschema.", + "examples": ["3DES", "Blowfish", "ECDH"] + }, "parameterSetIdentifier": { "type": "string", "title": "Parameter Set Identifier", "description": "An identifier for the parameter set of the cryptographic algorithm. Examples: in AES128, '128' identifies the key length in bits, in SHA256, '256' identifies the digest length, '128' in SHAKE128 identifies its maximum security level in bits, and 'SHA2-128s' identifies a parameter set used in SLH-DSA (FIPS205)." }, "curve": { + "deprecated": true, "type": "string", "title": "Elliptic Curve", - "description": "The specific underlying Elliptic Curve (EC) definition employed which is an indicator of the level of security strength, performance and complexity. Absent an authoritative source of curve names, CycloneDX recommends using curve names as defined at [https://neuromancer.sk/std/](https://neuromancer.sk/std/), the source of which can be found at [https://github.com/J08nY/std-curves](https://github.com/J08nY/std-curves)." + "description": "[Deprecated] This will be removed in a future version. Use `@.ellipticCurve` instead.\nThe specific underlying Elliptic Curve (EC) definition employed which is an indicator of the level of security strength, performance and complexity. Absent an authoritative source of curve names, CycloneDX recommends using curve names as defined at [https://neuromancer.sk/std/](https://neuromancer.sk/std/), the source of which can be found at [https://github.com/J08nY/std-curves](https://github.com/J08nY/std-curves)." + }, + "ellipticCurve": { + "$ref": "cryptography-defs.schema.json#/definitions/ellipticCurvesEnum", + "title": "Elliptic Curve", + "description": "The specific underlying Elliptic Curve (EC) definition employed which is an indicator of the level of security strength, performance and complexity. If specified, this value must be one of the enumeration of valid elliptic curves identifiers defined in the `cryptography-defs.schema.json` subschema." }, "executionEnvironment": { "type": "string", @@ -5119,7 +5324,7 @@ }, "implementationPlatform": { "type": "string", - "title": "implementation platform", + "title": "Implementation platform", "description": "The target platform for which the algorithm is implemented. The implementation can be 'generic', running on any platform or for a specific platform.", "enum": [ "generic", @@ -5132,7 +5337,6 @@ "armv9-a", "armv9-m", "s390x", - "riscv64", "ppc64", "ppc64le", "other", @@ -5303,6 +5507,11 @@ "description": "Properties for cryptographic assets of asset type 'certificate'", "additionalProperties": false, "properties": { + "serialNumber": { + "type": "string", + "title": "Serial Number", + "description": "The serial number is a unique identifier for the certificate issued by a CA." + }, "subjectName": { "type": "string", "title": "Subject Name", @@ -5326,14 +5535,16 @@ "description": "The date and time according to ISO-8601 standard from which the certificate is not valid anymore" }, "signatureAlgorithmRef": { + "deprecated": true, "$ref": "#/definitions/refType", "title": "Algorithm Reference", - "description": "The bom-ref to signature algorithm used by the certificate" + "description": "[DEPRECATED] This will be removed in a future version. Use `@.relatedCryptographicAssets` instead.\nThe bom-ref to signature algorithm used by the certificate" }, "subjectPublicKeyRef": { + "deprecated": true, "$ref": "#/definitions/refType", "title": "Key reference", - "description": "The bom-ref to the public key of the subject" + "description": "[DEPRECATED] This will be removed in a future version. Use `@.relatedCryptographicAssets` instead.\nThe bom-ref to the public key of the subject" }, "certificateFormat": { "type": "string", @@ -5347,9 +5558,10 @@ ] }, "certificateExtension": { + "deprecated": true, "type": "string", "title": "Certificate File Extension", - "description": "The file extension of the certificate", + "description": "[DEPRECATED] This will be removed in a future version. Use `@.certificateFileExtension` instead.\nThe file extension of the certificate", "examples": [ "crt", "pem", @@ -5357,6 +5569,206 @@ "der", "p12" ] + }, + "certificateFileExtension": { + "type": "string", + "title": "Certificate File Extension", + "description": "The file extension of the certificate.", + "examples": [ + "crt", + "pem", + "cer", + "der", + "p12" + ] + }, + "fingerprint": { + "type": "object", + "$ref": "#/definitions/hash", + "title": "Certificate Fingerprint", + "description": "The fingerprint is a cryptographic hash of the certificate excluding it's signature." + }, + "certificateState": { + "type": "array", + "title": "Certificate Lifecycle State", + "description": "The certificate lifecycle is a comprehensive process that manages digital certificates from their initial creation to eventual expiration or revocation. It typically involves several stages", + "items": { + "type": "object", + "title": "State", + "description": "The state of the certificate.", + "oneOf": [ + { + "title": "Pre-Defined State", + "required": [ + "state" + ], + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "title": "State", + "description": "A pre-defined state in the certificate lifecycle.", + "enum": [ + "pre-activation", + "active", + "suspended", + "deactivated", + "revoked", + "destroyed" + ], + "meta:enum": { + "pre-activation": "The certificate has been issued by the issuing certificate authority (CA) but has not been authorized for use.", + "active": "The certificate may be used to cryptographically protect information, cryptographically process previously protected information, or both.", + "deactivated": "Certificates in the deactivated state shall not be used to apply cryptographic protection but, in some cases, may be used to process cryptographically protected information.", + "suspended": "The use of a certificate may be suspended for several possible reasons.", + "revoked": "A revoked certificate is a digital certificate that has been invalidated by the issuing certificate authority (CA) before its scheduled expiration date.", + "destroyed": "The certificate has been destroyed." + } + }, + "reason": { + "type": "string", + "title": "Reason", + "description": "A reason for the certificate being in this state." + } + } + }, + { + "title": "Custom State", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "State", + "description": "The name of the certificate lifecycle state." + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the certificate lifecycle state." + }, + "reason": { + "type": "string", + "title": "Reason", + "description": "A reason for the certificate being in this state." + } + } + } + ] + } + }, + "creationDate": { + "type": "string", + "format": "date-time", + "title": "Creation Date", + "description": "The date and time (timestamp) when the certificate was created or pre-activated." + }, + "activationDate": { + "type": "string", + "format": "date-time", + "title": "Activation Date", + "description": "The date and time (timestamp) when the certificate was activated." + }, + "deactivationDate": { + "type": "string", + "format": "date-time", + "title": "Deactivation Date", + "description": "The date and time (timestamp) when the related certificate was deactivated." + }, + "revocationDate": { + "type": "string", + "format": "date-time", + "title": "Revocation Date", + "description": "The date and time (timestamp) when the certificate was revoked." + }, + "destructionDate": { + "type": "string", + "format": "date-time", + "title": "Destruction Date", + "description": "The date and time (timestamp) when the certificate was destroyed." + }, + "certificateExtensions": { + "type": "array", + "title": "Certificate Extensions", + "description": "A certificate extension is a field that provides additional information about the certificate or its use. Extensions are used to convey additional information beyond the standard fields.", + "items": { + "type": "object", + "title": "Extension", + "description": "", + "oneOf": [ + { + "title": "Common Extensions", + "required": [ + "commonExtensionName", + "commonExtensionValue" + ], + "additionalProperties": false, + "properties": { + "commonExtensionName": { + "type": "string", + "title": "name", + "description": "The name of the extension.", + "enum": [ + "basicConstraints", + "keyUsage", + "extendedKeyUsage", + "subjectAlternativeName", + "authorityKeyIdentifier", + "subjectKeyIdentifier", + "authorityInformationAccess", + "certificatePolicies", + "crlDistributionPoints", + "signedCertificateTimestamp" + ], + "meta:enum": { + "basicConstraints": "Specifies whether a certificate can be used as a CA certificate or not.", + "keyUsage": "Specifies the allowed uses of the public key in the certificate.", + "extendedKeyUsage": "Specifies additional purposes for which the public key can be used.", + "subjectAlternativeName": "Allows inclusion of additional names to identify the entity associated with the certificate.", + "authorityKeyIdentifier": "Identifies the public key of the CA that issued the certificate.", + "subjectKeyIdentifier": "Identifies the public key associated with the entity the certificate was issued to.", + "authorityInformationAccess": "Contains CA issuers and OCSP information.", + "certificatePolicies": "Defines the policies under which the certificate was issued and can be used.", + "crlDistributionPoints": "Contains one or more URLs where a Certificate Revocation List (CRL) can be obtained.", + "signedCertificateTimestamp": "Shows that the certificate has been publicly logged, which helps prevent the issuance of rogue certificates by a CA. Log ID, timestamp and signature as proof." + } + }, + "commonExtensionValue": { + "type": "string", + "title": "Value", + "description": "The value of the certificate extension." + } + } + }, + { + "title": "Custom Extensions", + "description": "Custom extensions may convey application-specific or vendor-specific data not covered by standard extensions. The structure and semantics of custom extensions are typically defined outside of public standards. CycloneDX leverages properties to support this capability.", + "required": [ + "customExtensionName" + ], + "additionalProperties": false, + "properties": { + "customExtensionName": { + "type": "string", + "title": "Name", + "description": "The name for the custom certificate extension." + }, + "customExtensionValue": { + "type": "string", + "title": "Value", + "description": "The description of the custom certificate extension." + } + } + } + ] + } + }, + "relatedCryptographicAssets": { + "$ref": "#/definitions/relatedCryptographicAssets", + "title": "Related Cryptographic Assets", + "description": "A list of cryptographic assets related to this component." } } }, @@ -5414,71 +5826,83 @@ } }, "id": { - "type": "string", - "title": "ID", - "description": "The optional unique identifier for the related cryptographic material." + "type": "string", + "title": "ID", + "description": "The unique identifier for the related cryptographic material." }, "state": { - "type": "string", - "title": "State", - "description": "The key state as defined by NIST SP 800-57.", - "enum": [ - "pre-activation", - "active", - "suspended", - "deactivated", - "compromised", - "destroyed" - ] + "type": "string", + "title": "State", + "description": "The key state as defined by NIST SP 800-57.", + "enum": [ + "pre-activation", + "active", + "suspended", + "deactivated", + "compromised", + "destroyed" + ] }, "algorithmRef": { - "$ref": "#/definitions/refType", - "title": "Algorithm Reference", - "description": "The bom-ref to the algorithm used to generate the related cryptographic material." + "deprecated": true, + "$ref": "#/definitions/refType", + "title": "Algorithm Reference", + "description": "[DEPRECATED] Use `@.relatedCryptographicAssets` instead.\nThe bom-ref to the algorithm used to generate the related cryptographic material." }, "creationDate": { - "type": "string", - "format": "date-time", - "title": "Creation Date", - "description": "The date and time (timestamp) when the related cryptographic material was created." + "type": "string", + "format": "date-time", + "title": "Creation Date", + "description": "The date and time (timestamp) when the related cryptographic material was created." }, "activationDate": { - "type": "string", - "format": "date-time", - "title": "Activation Date", - "description": "The date and time (timestamp) when the related cryptographic material was activated." + "type": "string", + "format": "date-time", + "title": "Activation Date", + "description": "The date and time (timestamp) when the related cryptographic material was activated." }, "updateDate": { - "type": "string", - "format": "date-time", - "title": "Update Date", - "description": "The date and time (timestamp) when the related cryptographic material was updated." + "type": "string", + "format": "date-time", + "title": "Update Date", + "description": "The date and time (timestamp) when the related cryptographic material was updated." }, "expirationDate": { - "type": "string", - "format": "date-time", - "title": "Expiration Date", - "description": "The date and time (timestamp) when the related cryptographic material expires." + "type": "string", + "format": "date-time", + "title": "Expiration Date", + "description": "The date and time (timestamp) when the related cryptographic material expires." }, "value": { - "type": "string", - "title": "Value", - "description": "The associated value of the cryptographic material." + "type": "string", + "title": "Value", + "description": "The associated value of the cryptographic material." }, "size": { - "type": "integer", - "title":"Size", - "description": "The size of the cryptographic asset (in bits)." + "type": "integer", + "title": "Size", + "description": "The size of the cryptographic asset (in bits)." }, "format": { - "type": "string", - "title": "Format", - "description": "The format of the related cryptographic material (e.g. P8, PEM, DER)." + "type": "string", + "title": "Format", + "description": "The format of the related cryptographic material (e.g. P8, PEM, DER)." }, "securedBy": { - "$ref": "#/definitions/securedBy", - "title": "Secured By", - "description": "The mechanism by which the cryptographic asset is secured by." + "$ref": "#/definitions/securedBy", + "title": "Secured By", + "description": "The mechanism by which the cryptographic asset is secured by." + }, + "fingerprint": { + "type": "object", + "$ref": "#/definitions/hash", + "title": "Fingerprint", + "description": "The fingerprint is a cryptographic hash of the asset." + }, + "relatedCryptographicAssets": { + "$ref": "#/definitions/relatedCryptographicAssets", + "title": "Related Cryptographic Assets", + "description": "A list of cryptographic assets related to this component." } } }, @@ -5499,6 +5923,12 @@ "ike", "sstp", "wpa", + "dtls", + "quic", + "eap-aka", + "eap-aka-prime", + "prins", + "5g-aka", "other", "unknown" ], @@ -5509,6 +5939,12 @@ "ike": "Internet Key Exchange", "sstp": "Secure Socket Tunneling Protocol", "wpa": "Wi-Fi Protected Access", + "dtls": "Datagram Transport Layer Security", + "quic": "Quick UDP Internet Connections", + "eap-aka": "Extensible Authentication Protocol variant", + "eap-aka-prime": "Enhanced version of EAP-AKA", + "prins": "Protection of Inter-Network Signaling", + "5g-aka": "Authentication and Key Agreement for 5G", "other": "Another protocol type", "unknown": "The protocol type is not known" } @@ -5535,45 +5971,122 @@ "ikev2TransformTypes": { "type": "object", "title": "IKEv2 Transform Types", - "description": "The IKEv2 transform types supported (types 1-4), defined in RFC7296 section 3.3.2, and additional properties.", + "description": "The IKEv2 transform types supported (types 1-4), defined in [RFC 7296 section 3.3.2](https://www.ietf.org/rfc/rfc7296.html#section-3.3.2), and additional properties.", "additionalProperties": false, "properties": { "encr": { - "$ref": "#/definitions/cryptoRefArray", - "title": "Encryption Algorithm (ENCR)", - "description": "Transform Type 1: encryption algorithms" + "title": "Encryption Algorithms (ENCR)", + "description": "Transform Type 1: encryption algorithms", + "anyOf": [ + { + "type": "array", + "title": "Encryption Algorithms (ENCR)", + "items": { + "$ref": "#/definitions/ikeV2Enc", + "title": "Encryption Algorithm (ENCR)" + } + }, + { + "deprecated": true, + "$ref": "#/definitions/cryptoRefArray", + "title": "Encryption Algorithm (ENCR) References", + "description": "[DEPRECATED] This will be removed in a future version.\nTransform Type 1: encryption algorithms" + } + ] }, "prf": { - "$ref": "#/definitions/cryptoRefArray", - "title": "Pseudorandom Function (PRF)", - "description": "Transform Type 2: pseudorandom functions" + "title": "Pseudorandom Functions (PRF)", + "description": "Transform Type 2: pseudorandom functions", + "anyOf": [ + { + "type": "array", + "title": "Pseudorandom Functions (PRF)", + "items": { + "$ref": "#/definitions/ikeV2Prf", + "title": "Pseudorandom Function (PRF)" + } + }, + { + "deprecated": true, + "$ref": "#/definitions/cryptoRefArray", + "description": "[DEPRECATED] This will be removed in a future version.\nTransform Type 2: pseudorandom functions" + } + ] }, "integ": { - "$ref": "#/definitions/cryptoRefArray", - "title": "Integrity Algorithm (INTEG)", - "description": "Transform Type 3: integrity algorithms" + "title": "Integrity Algorithms (INTEG)", + "description": "Transform Type 3: integrity algorithms", + "anyOf": [ + { + "type": "array", + "title": "Integrity Algorithms (INTEG)", + "items": { + "$ref": "#/definitions/ikeV2Integ", + "title": "Integrity Algorithm (INTEG)" + } + }, + { + "deprecated": true, + "$ref": "#/definitions/cryptoRefArray", + "description": "[DEPRECATED] This will be removed in a future version.\nTransform Type 3: integrity algorithms" + } + ] }, "ke": { - "$ref": "#/definitions/cryptoRefArray", - "title": "Key Exchange Method (KE)", - "description": "Transform Type 4: Key Exchange Method (KE) per RFC9370, formerly called Diffie-Hellman Group (D-H)" + "title": "Key Exchange Methods (KE)", + "description": "Transform Type 4: Key Exchange Method (KE) per [RFC 9370](https://www.ietf.org/rfc/rfc9370.html), formerly called Diffie-Hellman Group (D-H).", + "anyOf": [ + { + "type": "array", + "title": "Key Exchange Methods (KE)", + "items": { + "$ref": "#/definitions/ikeV2Ke", + "title": "Key Exchange Method (KE)" + } + }, + { + "deprecated": true, + "$ref": "#/definitions/cryptoRefArray", + "description": "[DEPRECATED] This will be removed in a future version.\nTransform Type 4: Key Exchange Method (KE) per [RFC 9370](https://www.ietf.org/rfc/rfc9370.html), formerly called Diffie-Hellman Group (D-H)." + } + ] }, "esn": { "type": "boolean", - "title": "Extended Sequence Numbers (ESN)", + "title": "Extended Sequence Number (ESN)", "description": "Specifies if an Extended Sequence Number (ESN) is used." }, "auth": { - "$ref": "#/definitions/cryptoRefArray", - "title": "IKEv2 Authentication method", - "description": "IKEv2 Authentication method" + "title": "IKEv2 Authentication methods", + "description": "IKEv2 Authentication method per [RFC9593](https://www.ietf.org/rfc/rfc9593.html).", + "anyOf": [ + { + "type": "array", + "title": "IKEv2 Authentication Methods", + "items": { + "$ref": "#/definitions/ikeV2Auth", + "title": "IKEv2 Authentication Method" + } + }, + { + "deprecated": true, + "$ref": "#/definitions/cryptoRefArray", + "description": "[DEPRECATED] This will be removed in a future version.\nIKEv2 Authentication method" + } + ] } } }, "cryptoRefArray": { + "deprecated": true, "$ref": "#/definitions/cryptoRefArray", "title": "Cryptographic References", - "description": "A list of protocol-related cryptographic assets" + "description": "[DEPRECATED] Use `@.relatedCryptographicAssets` instead.\nA list of protocol-related cryptographic assets" + }, + "relatedCryptographicAssets": { + "$ref": "#/definitions/relatedCryptographicAssets", + "title": "Related Cryptographic Assets", + "description": "A list of cryptographic assets related to this component." } } }, @@ -5621,15 +6134,183 @@ "0x9E" ] } + }, + "tlsGroups": { + "type": "array", + "title": "TLS Groups", + "description": "A list of TLS named groups (formerly known as curves) for this cipher suite. These groups define the parameters for key exchange algorithms like ECDHE.", + "items": { + "type": "string", + "title": "Group Name", + "description": "The name of the TLS group", + "examples": [ + "x25519", + "ffdhe2048" + ] + } + }, + "tlsSignatureSchemes": { + "type": "array", + "title": "TLS Signature Schemes", + "description": "A list of signature schemes supported for cipher suite. These schemes specify the algorithms used for digital signatures in TLS handshakes and certificate verification.", + "items": { + "type": "string", + "title": "Signature Scheme", + "description": "The name of the TLS signature scheme", + "examples": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "ed25519" + ] + } + } + } + }, + "ikeV2Enc": { + "type": "object", + "title": "Encryption Algorithm (ENCR)", + "description": "Object representing an encryption algorithm (ENCR)", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "A name for the encryption method.", + "examples": [ + "ENCR_AES_GCM_16" + ] + }, + "keyLength": { + "type": "integer", + "title": "Encryption algorithm key length", + "description": "The key length of the encryption algorithm." + }, + "algorithm": { + "$ref": "#/definitions/refType", + "title": "Algorithm reference", + "description": "The bom-ref to algorithm cryptographic asset." + } + } + }, + "ikeV2Prf": { + "type": "object", + "title": "Pseudorandom Function (PRF)", + "description": "Object representing a pseudorandom function (PRF)", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "A name for the pseudorandom function.", + "examples": [ + "PRF_HMAC_SHA2_256" + ] + }, + "algorithm": { + "$ref": "#/definitions/refType", + "title": "Algorithm reference", + "description": "The bom-ref to algorithm cryptographic asset." + } + } + }, + "ikeV2Integ": { + "type": "object", + "title": "Integrity Algorithm (INTEG)", + "description": "Object representing an integrity algorithm (INTEG)", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "A name for the integrity algorithm.", + "examples": [ + "AUTH_HMAC_SHA2_256_128" + ] + }, + "algorithm": { + "$ref": "#/definitions/refType", + "title": "Algorithm reference", + "description": "The bom-ref to algorithm cryptographic asset." + } + } + }, + "ikeV2Ke": { + "type": "object", + "title": "Key Exchange Method (KE)", + "description": "Object representing a key exchange method (KE)", + "additionalProperties": false, + "properties": { + "group": { + "type": "integer", + "title": "Group Identifier", + "description": "A group identifier for the key exchange algorithm." + }, + "algorithm": { + "$ref": "#/definitions/refType", + "title": "Algorithm reference", + "description": "The bom-ref to algorithm cryptographic asset." + } + } + }, + "ikeV2Auth": { + "type": "object", + "title": "IKEv2 Authentication method", + "description": "Object representing a IKEv2 Authentication method", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "A name for the authentication method." + }, + "algorithm": { + "$ref": "#/definitions/refType", + "title": "Algorithm reference", + "description": "The bom-ref to algorithm cryptographic asset." } } }, "cryptoRefArray" : { + "deprecated": true, + "title": "Encryption Algorithm (ENCR) Reference Array", + "description": "Deprecated definition.", "type": "array", "items": { "$ref": "#/definitions/refType" } }, + "relatedCryptographicAssets": { + "type": "array", + "title": "Related Cryptographic Assets", + "description": "A list of cryptographic assets related to this component.", + "items": { + "$ref": "#/definitions/relatedCryptographicAsset", + "title": "Related Cryptographic Asset" + } + }, + "relatedCryptographicAsset": { + "type": "object", + "title": "Related Cryptographic Asset", + "description": "A cryptographic assets related to this component.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "title": "Type", + "description": "Specifies the mechanism by which the cryptographic asset is secured by.", + "examples": [ + "publicKey", + "privateKey", + "algorithm" + ] + }, + "ref": { + "$ref": "#/definitions/refType", + "title": "Reference to cryptographic asset", + "description": "The bom-ref to cryptographic asset." + } + } + }, "securedBy": { "type": "object", "title": "Secured By", @@ -5669,6 +6350,351 @@ "translation", "object-detection" ] + }, + "patentFamily": { + "type": "object", + "title": "Patent Family", + "description": "A patent family is a group of related patent applications or granted patents that cover the same or similar invention. These patents are filed in multiple jurisdictions to protect the invention across different regions or countries. A patent family typically includes patents that share a common priority date, originating from the same initial application, and may vary slightly in scope or claims to comply with regional legal frameworks. Fields align with WIPO ST.96 standards where applicable.", + "required": ["familyId"], + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM. \n\nFor a patent, it might be a good idea to use a patent number as the BOM reference ID." + }, + "familyId": { + "type": "string", + "title": "Patent Family ID", + "description": "The unique identifier for the patent family, aligned with the `id` attribute in WIPO ST.96 v8.0's `PatentFamilyType`. Refer to [PatentFamilyType in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/PatentFamilyType.xsd)." + }, + "priorityApplication": { + "$ref": "#/definitions/priorityApplication" + }, + "members": { + "type": "array", + "title": "Family Members", + "description": "A collection of patents or applications that belong to this family, each identified by a `bom-ref` pointing to a patent object defined elsewhere in the BOM.", + "items": { + "$ref": "#/definitions/refLinkType", + "title": "BOM Reference", + "description": "A `bom-ref` linking to a patent or application object within the BOM." + } + }, + "externalReferences": { + "type": "array", + "title": "External References", + "description": "External references provide a way to document systems, sites, and information that may be relevant but are not included with the BOM. They may also establish specific relationships within or external to the BOM.", + "items": { + "$ref": "#/definitions/externalReference" + } + } + } + }, + "patent": { + "type": "object", + "title": "Patent", + "description": "A patent is a legal instrument, granted by an authority, that confers certain rights over an invention for a specified period, contingent on public disclosure and adherence to relevant legal requirements. The summary information in this object is aligned with [WIPO ST.96](https://www.wipo.int/standards/en/st96/) principles where applicable.", + "required": ["patentNumber", "jurisdiction", "patentLegalStatus"], + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An identifier which can be used to reference the object elsewhere in the BOM. Every `bom-ref` must be unique within the BOM." + }, + "patentNumber": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9\\-/.()\\s]{0,28}[A-Za-z0-9]$", + "title": "Patent Number", + "description": "The unique number assigned to the granted patent by the issuing authority. Aligned with `PatentNumber` in WIPO ST.96. Refer to [PatentNumber in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/PatentNumber.xsd).", + "examples": ["US987654321", "EP1234567B1"] + }, + "applicationNumber": { + "$ref": "#/definitions/patentApplicationNumber" + }, + "jurisdiction": { + "$ref": "#/definitions/patentJurisdiction" + }, + "priorityApplication": { + "$ref": "#/definitions/priorityApplication" + }, + "publicationNumber": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9\\-/.()\\s]{0,28}[A-Za-z0-9]$", + "title": "Patent Publication Number", + "description": "This is the number assigned to a patent application once it is published. Patent applications are generally published 18 months after filing (unless an applicant requests non-publication). This number is distinct from the application number. \n\nPurpose: Identifies the publicly available version of the application. \n\nFormat: Varies by jurisdiction, often similar to application numbers but includes an additional suffix indicating publication. \n\nExample:\n - US: US20240000123A1 (indicates the first publication of application US20240000123) \n - Europe: EP23123456A1 (first publication of European application EP23123456). \n\nWIPO ST.96 v8.0: \n - Publication Number field: https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/PublicationNumber.xsd" + }, + "title": { + "type": "string", + "title": "Patent Title", + "description": "The title of the patent, summarising the invention it protects. Aligned with `InventionTitle` in WIPO ST.96. Refer to [InventionTitle in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/InventionTitle.xsd)." + }, + "abstract": { + "type": "string", + "title": "Patent Abstract", + "description": "A brief summary of the invention described in the patent. Aligned with `Abstract` and `P` in WIPO ST.96. Refer to [Abstract in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/Abstract.xsd)." + }, + "filingDate": { + "type": "string", + "format": "date", + "title": "Filing Date", + "description": "The date the patent application was filed with the jurisdiction. Aligned with `FilingDate` in WIPO ST.96. Refer to [FilingDate in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/FilingDate.xsd)." + }, + "grantDate": { + "type": "string", + "format": "date", + "title": "Grant Date", + "description": "The date the patent was granted by the jurisdiction. Aligned with `GrantDate` in WIPO ST.96. Refer to [GrantDate in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/GrantDate.xsd)." + }, + "patentExpirationDate": { + "type": "string", + "format": "date", + "title": "Expiration Date", + "description": "The date the patent expires. Derived from grant or filing date according to jurisdiction-specific rules." + }, + "patentLegalStatus": { + "type": "string", + "title": "Legal Status", + "description": "Indicates the current legal status of the patent or patent application, based on the WIPO ST.27 standard. This status reflects administrative, procedural, or legal events. Values include both active and inactive states and are useful for determining enforceability, procedural history, and maintenance status.", + "enum": [ + "pending", + "granted", + "revoked", + "expired", + "lapsed", + "withdrawn", + "abandoned", + "suspended", + "reinstated", + "opposed", + "terminated", + "invalidated", + "in-force" + ], + "meta:enum": { + "pending": "The patent application has been filed but not yet examined or granted.", + "granted": "The patent application has been examined and a patent has been issued.", + "revoked": "The patent has been declared invalid through a legal or administrative process.", + "expired": "The patent has reached the end of its enforceable term.", + "lapsed": "The patent is no longer in force due to non-payment of maintenance fees or other requirements.", + "withdrawn": "The patent application was voluntarily withdrawn by the applicant.", + "abandoned": "The patent application was abandoned, often due to lack of action or response.", + "suspended": "Processing of the patent application has been temporarily halted.", + "reinstated": "A previously abandoned or lapsed patent has been reinstated.", + "opposed": "The patent application or granted patent is under formal opposition proceedings.", + "terminated": "The patent or application has been officially terminated.", + "invalidated": "The patent has been invalidated, either in part or in full.", + "in-force": "The granted patent is active and enforceable." + } + }, + "patentAssignee": { + "type": "array", + "title": "Patent Assignees", + "description": "A collection of organisations or individuals to whom the patent rights are assigned. This supports joint ownership and allows for flexible representation of both corporate entities and individual inventors.", + "items": { + "oneOf": [ + { + "title": "Person", + "$ref": "#/definitions/organizationalContact" + }, + { + "title": "Organizational Entity", + "$ref": "#/definitions/organizationalEntity" + } + ] + } + }, + "externalReferences": { + "type": "array", + "title": "External References", + "description": "External references provide a way to document systems, sites, and information that may be relevant but are not included with the BOM. They may also establish specific relationships within or external to the BOM.", + "items": { + "$ref": "#/definitions/externalReference" + } + } + } + }, + "patentAssertions": { + "type": "array", + "title": "Patent Assertions", + "description": "A list of assertions made regarding patents associated with this component or service. Assertions distinguish between ownership, licensing, and other relevant interactions with patents.", + "items": { + "type": "object", + "title": "Patent Assertion", + "description": "An assertion linking a patent or patent family to this component or service.", + "required": ["assertionType", "asserter"], + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "A reference to the patent or patent family object within the BOM. This must match the `bom-ref` of a `patent` or `patentFamily` object." + }, + "assertionType": { + "type": "string", + "title": "Assertion Type", + "description": "The type of assertion being made about the patent or patent family. Examples include ownership, licensing, and standards inclusion.", + "enum": [ + "ownership", + "license", + "third-party-claim", + "standards-inclusion", + "prior-art", + "exclusive-rights", + "non-assertion", + "research-or-evaluation" + ], + "meta:enum": { + "ownership": "The manufacturer asserts ownership of the patent or patent family.", + "license": "The manufacturer asserts they have a license to use the patent or patent family.", + "third-party-claim": "A third party has asserted a claim or potential infringement against the manufacturer’s component or service.", + "standards-inclusion": "The patent is part of a standard essential patent (SEP) portfolio relevant to the component or service.", + "prior-art": "The manufacturer asserts the patent or patent family as prior art that invalidates another patent or claim.", + "exclusive-rights": "The manufacturer asserts exclusive rights granted through a licensing agreement.", + "non-assertion": "The manufacturer asserts they will not enforce the patent or patent family against certain uses or users.", + "research-or-evaluation": "The patent or patent family is being used under a research or evaluation license." + } + }, + "patentRefs": { + "type": "array", + "title": "Patent References", + "description": "A list of BOM references (`bom-ref`) linking to patents or patent families associated with this assertion.", + "items": { + "$ref": "#/definitions/refType" + } + }, + "asserter": { + "oneOf": [ + { + "$ref": "#/definitions/organizationalEntity", + "title": "Organizational Entity" + }, + { + "$ref": "#/definitions/organizationalContact", + "title": "Person" + }, + { + "$ref": "#/definitions/refLinkType", + "title": "Reference", + "description": "A reference to a previously defined `organizationalContact` or `organizationalEntity` object in the BOM. The value must be a valid `bom-ref` pointing to one of these objects." + } + ] + }, + "notes": { + "type": "string", + "title": "Notes", + "description": "Additional notes or clarifications regarding the assertion, if necessary. For example, geographical restrictions, duration, or limitations of a license." + } + } + } + }, + "patentApplicationNumber": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9\\-/.()\\s]{0,28}[A-Za-z0-9]$", + "title": "Patent Application Number", + "description": "The unique number assigned to a patent application when it is filed with a patent office. It is used to identify the specific application and track its progress through the examination process. Aligned with `ApplicationNumber` in ST.96. Refer to [ApplicationIdentificationType in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/ApplicationIdentificationType.xsd).", + "examples": ["US20240000123", "EP23123456"] + }, + "patentJurisdiction": { + "type": "string", + "title": "Jurisdiction", + "description": "The jurisdiction or patent office where the priority application was filed, specified using WIPO ST.3 codes. Aligned with `IPOfficeCode` in ST.96. Refer to [IPOfficeCode in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Common/IPOfficeCode.xsd).", + "pattern": "^[A-Z]{2}$", + "examples": ["US", "EP", "JP"] + }, + "patentFilingDate": { + "type": "string", + "format": "date", + "title": "Filing Date", + "description": "The date the priority application was filed, aligned with `FilingDate` in ST.96. Refer to [FilingDate in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/FilingDate.xsd)." + }, + "priorityApplication": { + "type": "object", + "title": "Priority Application", + "description": "The priorityApplication contains the essential data necessary to identify and reference an earlier patent filing for priority rights. In line with WIPO ST.96 guidelines, it includes the jurisdiction (office code), application number, and filing date-the three key elements that uniquely specify the priority application in a global patent context.", + "required": ["applicationNumber", "jurisdiction", "filingDate"], + "additionalProperties": false, + "properties": { + "applicationNumber": { + "$ref": "#/definitions/patentApplicationNumber" + }, + "jurisdiction": { + "$ref": "#/definitions/patentJurisdiction" + }, + "filingDate": { + "$ref": "#/definitions/patentFilingDate" + } + } + }, + "citation": { + "type": "object", + "title": "Citation", + "description": "Details a specific attribution of data within the BOM to a contributing entity or process.", + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference" + }, + "pointers": { + "type": "array", + "items": { + "type": "string", + "title": "Field Reference", + "description": "A [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) identifying the BOM field to which the attribution applies.\nUsers of other serialization formats (e.g. XML) shall use the JSON Pointer format to ensure consistent field referencing across representations." + }, + "minItems": 1, + "title": "Field References", + "description": "One or more [JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901) identifying the BOM fields to which the attribution applies.\nExactly one of the \"pointers\" or \"expressions\" elements must be present." + }, + "expressions": { + "type": "array", + "items": { + "type": "string", + "title": "Path Expression", + "description": "Specifies a path expression used to locate a value within a BOM. The expression syntax shall conform to the format of the BOM's serialization.\nUse [JSONPath](https://datatracker.ietf.org/doc/html/rfc9535) for JSON, [XPath](https://www.w3.org/TR/xpath/) for XML, and default to JSONPath for Protocol Buffers unless otherwise specified.\nImplementers shall ensure the expression is valid within the context of the applicable serialization format." + }, + "minItems": 1, + "title": "Path Expressions", + "description": "One or more path expressions used to locate values within a BOM.\nExactly one of the \"pointers\" or \"expressions\" elements must be present." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp", + "description": "The date and time when the attribution was made or the information was supplied." + }, + "attributedTo": { + "$ref": "#/definitions/refLinkType", + "title": "Attributed To", + "description": "The `bom-ref` of an object, such as a component, service, tool, organisational entity, or person that supplied the cited information.\nAt least one of the \"attributedTo\" or \"process\" elements must be present." + }, + "process": { + "$ref": "#/definitions/refLinkType", + "title": "Process Reference", + "description": "The `bom-ref` to a process (such as a formula, workflow, task, or step) defined in the `formulation` section that executed or generated the attributed data.\nAt least one of the \"attributedTo\" or \"process\" elements must be present." + }, + "note": { + "type": "string", + "title": "Note", + "description": "A description or comment about the context or quality of the data attribution." + }, + "signature": { + "$ref": "#/definitions/signature", + "title": "Signature", + "description": "A digital signature verifying the authenticity or integrity of the attribution." + } + }, + "required": ["timestamp"], + "anyOf": [ + { "required": ["attributedTo"] }, + { "required": ["process"] } + ], + "oneOf": [ + { "required": ["pointers"] }, + { "required": ["expressions"] } + ] } } -} +} \ No newline at end of file diff --git a/schema/cyclonedx/cyclonedx.xsd b/schema/cyclonedx/cyclonedx.xsd index 21a591414..e7e13f796 100644 --- a/schema/cyclonedx/cyclonedx.xsd +++ b/schema/cyclonedx/cyclonedx.xsd @@ -16,13 +16,13 @@ limitations under the License. --> + version="1.7.1"> @@ -76,7 +76,7 @@ limitations under the License. - DEPRECATED. Use tools\components or tools\services instead. + DEPRECATED. Use `../components` or `../services` instead. @@ -253,9 +253,29 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. + + + + Conditions and constraints governing the sharing and distribution of the data or components + described by this BOM. + + + + + + + + The Traffic Light Protocol (TLP) classification that controls the sharing and + distribution of the data that the BOM describes. + + + + + + @@ -355,7 +375,10 @@ limitations under the License. - The URL of the organization. Multiple URLs are allowed. + + The URL of the organization. Multiple URLs are allowed. + Example: https://example.com + @@ -374,7 +397,7 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere in the BOM. + An identifier which can be used to reference the object elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -387,6 +410,52 @@ limitations under the License. + + + + Traffic Light Protocol (TLP) is a classification system for identifying the potential risk associated with artefact, including whether it is subject to certain types of legal, financial, or technical threats. Refer to https://www.first.org/tlp/ for further information. + The default classification is "CLEAR" + + + + + + + The information is not subject to any restrictions as regards the sharing. + + + + + + + The information is subject to limited disclosure, and recipients can share it within their community but not via publicly accessible channels. + + + + + + + The information is subject to limited disclosure, and recipients can only share it on a need-to-know basis within their organization and with clients. + + + + + + + The information is subject to limited disclosure, and recipients can only share it on a need-to-know basis within their organization. + + + + + + + The information is subject to restricted distribution to individual recipients only and must not be shared. + + + + + + Information about the automated or manual tool used @@ -463,7 +532,7 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere in the BOM. + An identifier which can be used to reference the object elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -551,12 +620,26 @@ limitations under the License. of the component. Examples: commons-lang3 and jquery - + - The component version. The version should ideally comply with semantic versioning - but is not enforced. + Must be used exclusively, either 'version' or 'versionRange', but not both. - + + + The component version. The version should ideally comply with semantic versioning + but is not enforced. + + + + + + + + Specifies a description for the component @@ -569,6 +652,9 @@ limitations under the License. + + The hashes of the component. + @@ -578,10 +664,21 @@ limitations under the License. - A copyright notice informing users of the underlying claims to - copyright ownership in a published work. + A copyright notice informing users of the underlying claims to copyright ownership in a published work. + + + + A list of assertions made regarding patents associated with this component or service. Assertions distinguish between ownership, licensing, and other relevant interactions with patents. + + + + + + + + @@ -592,7 +689,7 @@ limitations under the License. - Specifies the package-url (purl). The purl, if specified, MUST be valid and conform + Specifies the package-url (purl). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec @@ -600,7 +697,7 @@ limitations under the License. - Specifies the OmniBOR Artifact ID. The OmniBOR, if specified, MUST be valid and conform + Specifies the OmniBOR Artifact ID. The OmniBOR, if specified, must be valid and conform to the specification defined at: https://www.iana.org/assignments/uri-schemes/prov/gitoid @@ -608,7 +705,7 @@ limitations under the License. - Specifies the Software Heritage persistent identifier (SWHID). The SWHID, if specified, MUST + Specifies the Software Heritage persistent identifier (SWHID). The SWHID, if specified, must be valid and conform to the specification defined at: https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html @@ -653,7 +750,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -684,7 +781,7 @@ limitations under the License. - Specifies optional release notes. + Specifies release notes. @@ -693,12 +790,12 @@ limitations under the License. limitations, including biases and ethical considerations. Model cards typically contain the training parameters, which datasets were used to train the model, performance metrics, and other relevant data useful for ML transparency. This object SHOULD be specified for any component of - type `machine-learning-model` and MUST NOT be specified for other component types. + type `machine-learning-model` and must not be specified for other component types. - + - This object SHOULD be specified for any component of type `data` and MUST NOT be + This object SHOULD be specified for any component of type `data` and must not be specified for other component types. @@ -734,16 +831,25 @@ limitations under the License. - The OPTIONAL mime-type of the component. When used on file components, the mime-type + The mime-type of the component. When used on file components, the mime-type can provide additional context about the kind of file being represented such as an image, font, or executable. Some library or framework components may also have an associated mime-type. + + + + Determine whether this component is external. + An external component is one that is not part of an assembly, but is expected to be provided by the environment, regardless of the component's `@scope`. This setting can be useful for distinguishing which components are bundled with the product and which can be relied upon to be present in the deployment environment. + This may be set to `true` for runtime components only. For `/metadata/component`, it must be set to `false`. + + + - An optional identifier which can be used to reference the component elsewhere in the BOM. + An identifier which can be used to reference the component elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -754,180 +860,21 @@ limitations under the License. do not have the same name as an existing attribute used by the schema. + - - - - - - A valid SPDX license ID - - - - - If SPDX does not define the license used, this field may be used to provide the license name - - - - - - Specifies the optional full text of the attachment - - - - - The URL to the attachment file. If the attachment is a license or BOM, - an externalReference should also be specified for completeness. - - - - - Licensing details describing the licensor/licensee, license type, renewal and - expiration dates, and other important metadata - - - - - - License identifiers that may be used to manage licenses and - their lifecycle - - - - - - - - - - The individual or organization that grants a license to another - individual or organization - - - - - - - The organization that granted the license - - - - - The individual, not associated with an organization, - that granted the license - - - - - - - - - The individual or organization for which a license was granted to - - - - - - - The organization that was granted the license - - - - - The individual, not associated with an organization, - that was granted the license - - - - - - - - - The individual or organization that purchased the license - - - - - - - The organization that purchased the license - - - - - The individual, not associated with an organization, - that purchased the license - - - - - - - - - The purchase order identifier the purchaser sent to a supplier or - vendor to authorize a purchase - - - - - The type of license(s) that was granted to the licensee - - - - - - - - - - The timestamp indicating when the license was last - renewed. For new purchases, this is often the purchase or acquisition date. - For non-perpetual licenses or subscriptions, this is the timestamp of when the - license was last renewed. - - - - - The timestamp indicating when the current license - expires (if applicable). - - - - - - Allows any undeclared elements as long as the elements are placed in a different namespace. - - - - - - - - - Provides the ability to document properties in a name/value store. - This provides flexibility to include data not officially supported in the standard - without having to use additional namespaces or create extensions. Property names - of interest to the general public are encouraged to be registered in the - CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. - - - - - - Allows any undeclared elements as long as the elements are placed in a different namespace. - - - - + - An optional identifier which can be used to reference the license elsewhere in the BOM. + An identifier which can be used to reference the license elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -949,6 +896,301 @@ limitations under the License. + + + + + + + License identifiers that may be used to manage licenses and + their lifecycle + + + + + + + + + + The individual or organization that grants a license to another + individual or organization + + + + + + + The organization that granted the license + + + + + The individual, not associated with an organization, + that granted the license + + + + + + + + + The individual or organization for which a license was granted to + + + + + + + The organization that was granted the license + + + + + The individual, not associated with an organization, + that was granted the license + + + + + + + + + The individual or organization that purchased the license + + + + + + + The organization that purchased the license + + + + + The individual, not associated with an organization, + that purchased the license + + + + + + + + + The purchase order identifier the purchaser sent to a supplier or + vendor to authorize a purchase + + + + + The type of license(s) that was granted to the licensee + + + + + + + + + + The timestamp indicating when the license was last + renewed. For new purchases, this is often the purchase or acquisition date. + For non-perpetual licenses or subscriptions, this is the timestamp of when the + license was last renewed. + + + + + The timestamp indicating when the current license + expires (if applicable). + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + + Specifies the details and attributes related to a software license. + It can either include a valid SPDX license identifier or a named license, along with additional + properties such as license acknowledgment, comprehensive commercial licensing information, and + the full text of the license. + + + + + + A valid SPDX license identifier. If specified, this value must be one of the enumeration of valid SPDX license identifiers defined in the spdx.schema.json (or spdx.xml) subschema which is synchronized with the official SPDX license list. + + + + + The name of the license. This may include the name of a commercial or proprietary license or an open source license that may not be defined by SPDX. + + + + + + Specifies the full text of the attachment + + + + + The URL to the attachment file. If the attachment is a license or BOM, + an externalReference should also be specified for completeness. + + + + + Licensing details describing the licensor/licensee, license type, renewal and + expiration dates, and other important metadata + + + + + Provides the ability to document properties in a name/value store. + This provides flexibility to include data not officially supported in the standard + without having to use additional namespaces or create extensions. Property names + of interest to the general public are encouraged to be registered in the + CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. + Formal registration is optional. + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + + + + + A valid SPDX license expression. + Refer to https://spdx.org/specifications for syntax requirements. + + Example values: + - Apache-2.0 AND (MIT OR GPL-2.0-only) + - GPL-3.0-only WITH Classpath-exception-2.0 + + + + + + + + + + + Specifies the details and attributes related to a software license. + It must be a valid SPDX license expression, along with additional properties such as license acknowledgment. + + + + + + Details for parts of the `expression`. + + + + + This document specifies the details and attributes related to a software license identifier. An SPDX expression may be a compound of license identifiers. + The `license-identifier` attribute serves as the key that identifies each record. Note that this key is not required to be unique, as the same license identifier could apply to multiple, different but similar license details, texts, etc. + + + + + + A way to include the textual content of the license. + + + + + The URL to the attachment file. If the attachment is a license or BOM, + an externalReference should also be specified for completeness. + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + A valid SPDX license identifier. Refer to https://spdx.org/specifications for syntax requirements. + This attribute serves as the primary key, which uniquely identifies each record. + + Example values: + - Apache-2.0 + - GPL-3.0-only WITH Classpath-exception-2.0 + - LicenseRef-my-custom-license + + + + + + + An identifier which can be used to reference the license elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + + + + + + Licensing details describing the licensor/licensee, license type, renewal and + expiration dates, and other important metadata + + + + + Provides the ability to document properties in a name/value store. + This provides flexibility to include data not officially supported in the standard + without having to use additional namespaces or create extensions. Property names + of interest to the general public are encouraged to be registered in the + CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. + Formal registration is optional. + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + A valid SPDX license expression. + Refer to https://spdx.org/specifications for syntax requirements. + + Example values: + - Apache-2.0 AND (MIT OR GPL-2.0-only) + - GPL-3.0-only WITH Classpath-exception-2.0 + + + @@ -959,14 +1201,20 @@ limitations under the License. - Specifies the content type of the text. Defaults to text/plain - if not specified. + + Specifies the format and nature of the data being attached, helping systems correctly + interpret and process the content. Common content type examples include `application/json` + for JSON data and `text/plain` for plain text documents. + RFC 2045 section 5.1 outlines the structure and use of content types. For a comprehensive + list of registered content types, refer to the IANA media types registry at + https://www.iana.org/assignments/media-types/media-types.xhtml. + - Specifies the optional encoding the text is represented in + Specifies the encoding the text is represented in @@ -1045,8 +1293,11 @@ limitations under the License. - A runtime environment which interprets or executes software. This may include - runtimes such as those that execute bytecode or low-code/no-code application platforms. + + A runtime environment that interprets or executes software. + This may include runtimes such as those that execute bytecode, just-in-time compilers, + interpreters, or low-code/no-code application platforms. + @@ -1117,6 +1368,8 @@ limitations under the License. + + @@ -1379,8 +1632,11 @@ limitations under the License. - The URL to the license file. If a license URL has been defined in the license - node, it should also be defined as an external reference for completeness + + The URL to the license file. If a license URL has been defined in the license + node, it should also be defined as an external reference for completeness. + Example: https://www.apache.org/licenses/LICENSE-2.0.txt + @@ -1427,7 +1683,7 @@ limitations under the License. - Describes how a component or service was manufactured or deployed. + Describes the formulation of any referencable object within the BOM, including components, services, metadata, declarations, or the BOM itself. @@ -1507,7 +1763,7 @@ limitations under the License. - Plans of Action and Milestones (POAM) complement an "attestation" external reference. POAM is defined by NIST as a "document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones". + Plans of Action and Milestones (POA&M) complement an "attestation" external reference. POA&M is defined by NIST as a "document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones". @@ -1525,6 +1781,26 @@ limitations under the License. Document that complies with RFC-9116 (A File Format to Aid in Security Vulnerability Disclosure) + + + References information about patents which may be defined in human-readable documents or in machine-readable formats such as CycloneDX or ST.96. For detailed patent information or to reference the information provided directly by patent offices, it is recommended to leverage standards from the World Intellectual Property Organization (WIPO) such as [ST.96](https://www.wipo.int/standards/en/st96). + + + + + References information about a patent family which may be defined in human-readable documents or in machine-readable formats such as CycloneDX or ST.96. A patent family is a group of related patent applications or granted patents that cover the same or similar invention. For detailed patent family information or to reference the information provided directly by patent offices, it is recommended to leverage standards from the World Intellectual Property Organization (WIPO) such as [ST.96](https://www.wipo.int/standards/en/st96). + + + + + References assertions made regarding patents associated with a component or service. Assertions distinguish between ownership, licensing, and other relevant interactions with patents. + + + + + A reference to external citations applicable to the object identified by this BOM entry or the BOM itself. When used with a BOM-Link, this allows offloading citations into a separate CycloneDX BOM. + + Use this if no other types accurately describe the purpose of the external reference @@ -1567,7 +1843,7 @@ limitations under the License. - An optional comment describing the external reference + A comment describing the external reference @@ -1577,6 +1853,16 @@ limitations under the License. + + + Provides the ability to document properties in a name/value store. + This provides flexibility to include data not officially supported in the standard + without having to use additional namespaces or create extensions. Property names + of interest to the general public are encouraged to be registered in the + CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. + Formal registration is optional. + + @@ -1758,7 +2044,7 @@ limitations under the License. - Specifies the optional text of the diff + Specifies the text of the diff @@ -1825,6 +2111,12 @@ limitations under the License. + + + A collection of URL's for reference. Multiple URLs are allowed. + Example: "https://example.com" + + @@ -1981,10 +2273,10 @@ limitations under the License. Defines the direct dependencies of a component or service. Components or services - that do not have their own dependencies MUST be declared as empty elements within the graph. - Components or services that are not represented in the dependency graph MAY have unknown - dependencies. It is RECOMMENDED that implementations assume this to be opaque and not an - indicator of a object being dependency-free. It is RECOMMENDED to leverage compositions to + that do not have their own dependencies must be declared as empty elements within the graph. + Components or services that are not represented in the dependency graph may have unknown + dependencies. It is recommended that implementations assume this to be opaque and not an + indicator of a object being dependency-free. It is recommended to leverage compositions to indicate unknown dependency graphs. @@ -2041,6 +2333,12 @@ limitations under the License. + + + The endpoint URIs of the service. Multiple endpoints are allowed. + Example: "https://example.com/api/v1/ticker" + + @@ -2071,12 +2369,15 @@ limitations under the License. + + Specifies information about the data including the directional flow of data and the data classification. + - DEPRECATED: Specifies the data classification. THIS FIELD IS DEPRECATED AS OF v1.5. Use dataflow\classification instead + DEPRECATED: Specifies the data classification. THIS FIELD IS DEPRECATED AS OF v1.5. Use `./dataflow/classification` instead @@ -2147,6 +2448,18 @@ limitations under the License. + + + + A list of assertions made regarding patents associated with this component or service. Assertions distinguish between ownership, licensing, and other relevant interactions with patents. + + + + + + + + Provides the ability to document external references related to the service. @@ -2159,7 +2472,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -2184,7 +2497,7 @@ limitations under the License. - Specifies optional release notes. + Specifies release notes. @@ -2199,7 +2512,7 @@ limitations under the License. - An optional identifier which can be used to reference the service elsewhere in the BOM. + An identifier which can be used to reference the service elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -2236,70 +2549,51 @@ limitations under the License. states that the direction is not known. - - - - + + + + Data that enters a service. + + + + + + Data that exits a service. + + + + + Data flows in and out of the service. + + + + + The directional flow of data is not known. + + - - - - - A valid SPDX license expression. - Refer to https://spdx.org/specifications for syntax requirements - - Example values: - - Apache-2.0 AND (MIT OR GPL-2.0-only) - - GPL-3.0-only WITH Classpath-exception-2.0 - - - - - - - - - An optional identifier which can be used to reference the license elsewhere in the BOM. - Uniqueness is enforced within all elements and children of the root-level bom element. - - - - - - - Declared licenses and concluded licenses represent two different stages in the - licensing process within software development. Declared licenses refer to the - initial intention of the software authors regarding the licensing terms under - which their code is released. On the other hand, concluded licenses are the - result of a comprehensive analysis of the project's codebase to identify and - confirm the actual licenses of the components used, which may differ from the - initially declared licenses. While declared licenses provide an upfront indication - of the licensing intentions, concluded licenses offer a more thorough understanding - of the actual licensing within a project, facilitating proper compliance and risk - management. Observed licenses are defined in `evidence.licenses`. Observed licenses - form the evidence necessary to substantiate a concluded license. - - - - - - - + + A list of SPDX licenses and/or named licenses and/or SPDX License Expression. + + + + + - - - Declared licenses represent the initial intentions of authors regarding - the licensing terms of their code. - - + + + Declared licenses represent the initial intentions of authors regarding + the licensing terms of their code. + + @@ -2419,7 +2713,7 @@ limitations under the License. Evidence that substantiates the identity of a component. The identify may be an object or an array of identity objects. Support for specifying identity as a single object was - introduced in CycloneDX v1.5. "unbounded" was introduced in v1.6. It is RECOMMENDED that all + introduced in CycloneDX v1.5. "unbounded" was introduced in v1.6. It is recommended that all implementations are aware of "unbounded". @@ -2525,8 +2819,8 @@ limitations under the License. - An optional identifier which can be used to reference the occurrence elsewhere - in the BOM. Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the occurrence elsewhere + in the BOM. Every `bom-ref` must be unique within the BOM. @@ -2545,6 +2839,11 @@ limitations under the License. + + + Within a call stack, a frame is a discrete unit that encapsulates an execution context, including local variables, parameters, and the return address. As function calls are made, frames are pushed onto the stack, forming an array-like structure that orchestrates the flow of program execution and manages the sequence of function invocations. + + @@ -2564,7 +2863,7 @@ limitations under the License. - Optional arguments that are passed to the module or function. + Arguments that are passed to the module or function. @@ -2611,7 +2910,13 @@ limitations under the License. - + + + + Copyright evidence captures intellectual property assertions, providing evidence of possible ownership and legal protection. + + + @@ -2651,7 +2956,7 @@ limitations under the License. - Specifies an aggregate type that describe how complete a relationship is. + Specifies an aggregate type that describes how complete a relationship is. @@ -2720,7 +3025,7 @@ limitations under the License. - An optional identifier which can be used to reference the composition elsewhere in the BOM. + An identifier which can be used to reference the composition elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -2746,12 +3051,12 @@ limitations under the License. - The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are proprietary. + The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented, limited specifically to those that are proprietary. - The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are opensource. + The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented, limited specifically to those that are opensource. @@ -2786,8 +3091,8 @@ limitations under the License. Defines a syntax for representing two character language code (ISO-639) followed by an optional two - character country code. The language code MUST be lower case. If the country code is specified, the - country code MUST be upper case. The language code and country code MUST be separated by a minus sign. + character country code. The language code must be lower case. If the country code is specified, the + country code must be upper case. The language code and country code must be separated by a minus sign. Examples: en, en-US, fr, fr-CA @@ -2800,7 +3105,7 @@ limitations under the License. - The software versioning type. It is RECOMMENDED that the release type use one + The software versioning type. It is recommended that the release type use one of 'major', 'minor', 'patch', 'pre-release', or 'internal'. Representing all possible software release types is not practical, so standardizing on the recommended values, whenever possible, is strongly encouraged. @@ -2808,9 +3113,9 @@ limitations under the License. * minor = A minor release, also known as an update, may contain a smaller number of changes than major releases. * patch = Patch releases are typically unplanned and may resolve defects or important security issues. * pre-release = A pre-release may include alpha, beta, or release candidates and typically have - limited support. They provide the ability to preview a release prior to its general availability. + limited support. They provide the ability to preview a release prior to its general availability. * internal = Internal releases are not for public consumption and are intended to be used exclusively - by the project or manufacturer that produced it. + by the project or manufacturer that produced it. @@ -2845,7 +3150,8 @@ limitations under the License. One or more alternate names the release may be referred to. This may - include unofficial terms used by development and marketing teams (e.g. code names). + include unofficial terms used by development and marketing teams (e.g. code names). + @@ -2868,7 +3174,8 @@ limitations under the License. Zero or more release notes containing the locale and content. Multiple - note elements may be specified to support release notes in a wide variety of languages. + note elements may be specified to support release notes in a wide variety of languages. + @@ -2896,7 +3203,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -2915,19 +3222,19 @@ limitations under the License. - + A model card describes the intended uses of a machine learning model and potential limitations, including biases and ethical considerations. Model cards typically contain the training parameters, which datasets were used to train the model, performance metrics, and other relevant data useful for ML transparency. - This object SHOULD be specified for any component of type `machine-learning-model` and MUST NOT be specified + This object SHOULD be specified for any component of type `machine-learning-model` and must not be specified for other component types. @@ -2997,7 +3304,11 @@ limitations under the License. - + + + Inline Data Information + + @@ -3155,7 +3466,7 @@ limitations under the License. - The graphic (vector or raster). Base64 encoding MUST be specified for binary images. + The graphic (vector or raster). Base64 encoding must be specified for binary images. @@ -3188,7 +3499,7 @@ limitations under the License. - + @@ -3200,7 +3511,7 @@ limitations under the License. - + @@ -3214,7 +3525,7 @@ limitations under the License. - + @@ -3226,7 +3537,7 @@ limitations under the License. - + @@ -3318,12 +3629,22 @@ limitations under the License. + + + Provides the ability to document properties in a name/value store. + This provides flexibility to include data not officially supported in the standard + without having to use additional namespaces or create extensions. Property names + of interest to the general public are encouraged to be registered in the + CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. + Formal registration is OPTIONAL. + + - An optional identifier which can be used to reference the model card elsewhere in the BOM. - Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the model card elsewhere in the BOM. + Every `bom-ref` must be unique within the BOM. @@ -3350,7 +3671,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -3492,7 +3813,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -3694,7 +4015,7 @@ limitations under the License. - An optional identifier which can be used to reference the energy provider elsewhere in the BOM. + An identifier which can be used to reference the energy provider elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -3754,7 +4075,7 @@ limitations under the License. - An optional identifier which can be used to reference the address elsewhere in the BOM. + An identifier which can be used to reference the address elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -3838,7 +4159,7 @@ limitations under the License. - An optional way to include textual or encoded data. + A way to include textual or encoded data. @@ -3861,7 +4182,7 @@ limitations under the License. - + A description of any sensitive data in a dataset. @@ -3888,8 +4209,8 @@ limitations under the License. - An optional identifier which can be used to reference the dataset elsewhere in the BOM. - Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the dataset elsewhere in the BOM. + Every `bom-ref` must be unique within the BOM. @@ -3950,7 +4271,7 @@ limitations under the License. - + A description of this collection of graphics. @@ -3978,7 +4299,7 @@ limitations under the License. - The graphic (vector or raster). Base64 encoding MUST be specified for binary images. + The graphic (vector or raster). Base64 encoding must be specified for binary images. @@ -4150,7 +4471,7 @@ limitations under the License. - List of vulnerability ratings. + List of vulnerability ratings. Consumers SHOULD consider ratings in prioritization decisions; source ratings may differ and aid prioritization. @@ -4201,9 +4522,9 @@ limitations under the License. - - Precise steps to reproduce the vulnerability. - + + Precise steps to reproduce the vulnerability. + @@ -4293,7 +4614,7 @@ limitations under the License. - DEPRECATED. Use tools\components or tools\services instead. + DEPRECATED. Use `../components` or `../services` instead. @@ -4407,7 +4728,7 @@ limitations under the License. - A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst + A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/vers-spec @@ -4437,14 +4758,14 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. - An optional identifier which can be used to reference the vulnerability elsewhere in the BOM. + An identifier which can be used to reference the vulnerability elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -4463,7 +4784,8 @@ limitations under the License. The url of the vulnerability documentation as provided by the source. - For example: https://nvd.nist.gov/vuln/detail/CVE-2021-39182 + For example: https://nvd.nist.gov/vuln/detail/CVE-2021-39182 + @@ -4498,7 +4820,7 @@ limitations under the License. - An optional reason for rating the vulnerability as it was. + A reason for rating the vulnerability as it was. @@ -4508,7 +4830,7 @@ limitations under the License. - An optional name of the advisory. + A name of the advisory. @@ -4611,7 +4933,7 @@ limitations under the License. - An optional identifier which can be used to reference the annotation elsewhere in the BOM. + An identifier which can be used to reference the annotation elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -4632,13 +4954,41 @@ limitations under the License. - - - - - - - + + + Critical severity + + + + + High severity + + + + + Medium severity + + + + + Low severity + + + + + Informational warning. + + + + + None + + + + + The severity is not known + + @@ -4835,11 +5185,31 @@ limitations under the License. - - - - - + + + Can not fix + + + + + Will not fix + + + + + Update to a different revision or release + + + + + Revert to a previous revision or release + + + + + There is a workaround available + + @@ -4854,18 +5224,34 @@ limitations under the License. - - - + + + The version is affected by the vulnerability. + + + + + The version is not affected by the vulnerability. + + + + + It is unknown (or unspecified) whether the given version is affected. + + - Describes how a component or service was manufactured or deployed. This is achieved through the use - of formulas, workflows, tasks, and steps, which declare the precise steps to reproduce along with the - observed formulas describing the steps which transpired in the manufacturing process. + Describes the formulation of any referencable object within the BOM, + including components, services, metadata, declarations, or the BOM itself. This may + encompass how the object was created, assembled, deployed, tested, certified, or otherwise + brought into its present form. Common examples include software build pipelines, + deployment processes, AI/ML model training, cryptographic key generation or certification, + and third-party audits. Processes are modeled using declared and observed formulas, + composed of workflows, tasks, and individual steps. @@ -4919,14 +5305,14 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. - An optional identifier which can be used to reference the formula elsewhere in the BOM. + An identifier which can be used to reference the formula elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -5080,7 +5466,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5094,7 +5480,7 @@ limitations under the License. - An optional identifier which can be used to reference the workflow elsewhere in the BOM. + An identifier which can be used to reference the workflow elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -5302,7 +5688,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5316,7 +5702,7 @@ limitations under the License. - An optional identifier which can be used to reference the task elsewhere in the BOM. + An identifier which can be used to reference the task elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -5505,7 +5891,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5519,7 +5905,7 @@ limitations under the License. - An optional identifier which can be used to reference the workflow elsewhere in the BOM. + An identifier which can be used to reference the workflow elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -5606,7 +5992,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5665,7 +6051,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5681,7 +6067,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5745,6 +6131,9 @@ limitations under the License. + + A list of conditions used to determine if a trigger should be activated. + @@ -5776,7 +6165,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5823,7 +6212,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5837,7 +6226,7 @@ limitations under the License. - An optional identifier which can be used to reference the trigger elsewhere in the BOM. + An identifier which can be used to reference the trigger elsewhere in the BOM. Uniqueness is enforced within all elements and children of the root-level bom element. @@ -5910,7 +6299,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -5998,7 +6387,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -6085,7 +6474,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -6365,6 +6754,14 @@ limitations under the License. + + + + Key-wrap is a cryptographic technique used to securely encrypt and + protect cryptographic keys using algorithms like AES. + + + @@ -6382,6 +6779,14 @@ limitations under the License. + + + + A valid algorithm family identifier. + If specified, this value must be one of the enumeration of valid algorithm Family identifiers defined in the `cryptography-defs.schema.json` subschema. A corresponding schema for XML is not available. + + + @@ -6395,6 +6800,7 @@ limitations under the License. + DEPRECATED - DO NOT USE. This will be removed in a future version. Use `./ellipticCurve` instead. The specific underlying Elliptic Curve (EC) definition employed which is an indicator of the level of security strength, performance and complexity. Absent an authoritative source of curve names, CycloneDX recommends use of curve names as @@ -6403,6 +6809,14 @@ limitations under the License. + + + + The specific underlying Elliptic Curve (EC) definition employed which is an indicator of the level of security strength, performance and complexity. + If specified, this value must be one of the enumeration of valid elliptic curves identifiers defined in the `cryptography-defs.schema.json` subschema. A corresponding schema for XML is not available. + + + @@ -6471,7 +6885,6 @@ limitations under the License. - @@ -6897,6 +7310,13 @@ limitations under the License. + + + + The serial number is a unique identifier for the certificate issued by a CA. + + + @@ -6928,6 +7348,7 @@ limitations under the License. + DEPRECATED - DO NOT USE. This will be removed in a future version. Use `./relatedCryptographicAssets` instead. The bom-ref to signature algorithm used by the certificate @@ -6935,6 +7356,7 @@ limitations under the License. + DEPRECATED - DO NOT USE. This will be removed in a future version. Use `./relatedCryptographicAssets` instead. The bom-ref to the public key of the subject @@ -6949,10 +7371,339 @@ limitations under the License. + DEPRECATED - DO NOT USE. This will be removed in a future version. Use `./certificateFileExtension` instead. The file extension of the certificate. Examples include crt, pem, cer, der, and p12. + + + + The file extension of the certificate. Examples include crt, pem, cer, der, and p12. + + + + + + + The fingerprint is a cryptographic hash of the certificate excluding it's signature. + + + + + + + The certificate lifecycle is a comprehensive process that manages digital + certificates from their initial creation to eventual expiration or revocation. + It typically involves several stages. + + + + + + + + + + A pre-defined state in the certificate lifecycle. + + + + + + + + The certificate has been issued by the issuing + certificate authority (CA) but has not been authorized + for use. + + + + + + + The certificate may be used to cryptographically protect + information, cryptographically process previously protected + information, or both. + + + + + + + Certificates in the deactivated state shall not be used + to apply cryptographic protection but, in some cases, + may be used to process cryptographically protected + information. + + + + + + + The use of a certificate may be suspended for several + possible reasons. + + + + + + + A revoked certificate is a digital certificate that has + been invalidated by the issuing certificate authority (CA) + before its scheduled expiration date. + + + + + + + The certificate has been destroyed. + + + + + + + + + + A reason for the certificate being in this state. + + + + + + + + + + The name of the certificate lifecycle state. + + + + + + + The description of the certificate lifecycle state. + + + + + + + A reason for the certificate being in this state. + + + + + + + + + + + The date and time (timestamp) when the certificate was created or pre-activated. + + + + + + + The date and time (timestamp) when the certificate was activated. + + + + + + + The date and time (timestamp) when the related certificate was deactivated. + + + + + + + The date and time (timestamp) when the certificate was revoked. + + + + + + + The date and time (timestamp) when the certificate was destroyed. + + + + + + + A certificate extension is a field that provides additional information about the certificate or its use. Extensions are used to convey additional information beyond the standard fields. + + + + + + + + Extension: This can be either a common extension + (with a well-known name and value) or a custom extension + (for application or vendor-specific data). + + + + + + + + + + The name of the extension. + + + + + + + + Specifies whether a certificate can be used as a CA certificate or not. + + + + + + + Specifies the allowed uses of the public key in the certificate. + + + + + + + Specifies additional purposes for which the public key can be used. + + + + + + + Allows inclusion of additional names to identify the entity associated with the certificate. + + + + + + + Identifies the public key of the CA that issued the certificate. + + + + + + + Identifies the public key associated with the entity the certificate was issued to. + + + + + + + Contains CA issuers and OCSP information. + + + + + + + Defines the policies under which the certificate was issued and can be used. + + + + + + + Contains one or more URLs where a Certificate Revocation List (CRL) can be obtained. + + + + + + + Shows that the certificate has been publicly logged, which helps prevent the issuance of rogue certificates by a CA. Log ID, timestamp and signature as proof. + + + + + + + + + + The value of the certificate extension. + + + + + + + + + + The name for the custom certificate extension. + + + + + + + The description of the custom certificate extension. + + + + + + + + + + + + + + A list of cryptographic assets related to this component. + + + + + + + + A cryptographic asset related to this component. + + + + + + + + Specifies the mechanism by which the cryptographic asset is secured by. + Examples: "publicKey", "privateKey", "algorithm" + + + + + + + The bom-ref to cryptographic asset. + + + + + + + + + @@ -6997,7 +7748,7 @@ limitations under the License. - The optional unique identifier for the related cryptographic material. + The unique identifier for the related cryptographic material. @@ -7100,6 +7851,50 @@ limitations under the License. + + + + The fingerprint is a cryptographic hash of the related cryptographic material, excluding it's signature. + + + + + + + A list of cryptographic assets related to this component. + + + + + + + + A cryptographic asset related to this component. + + + + + + + + Specifies the mechanism by which the cryptographic asset is secured by. + Examples: "publicKey", "privateKey", "algorithm" + + + + + + + The bom-ref to cryptographic asset. + + + + + + + + + @@ -7161,6 +7956,48 @@ limitations under the License. + + + + Datagram Transport Layer Security + + + + + + + Quick UDP Internet Connections + + + + + + + Extensible Authentication Protocol variant + + + + + + + Enhanced version of EAP-AKA + + + + + + + Protection of Inter-Network Signaling + + + + + + + Authentication and Key Agreement for 5G + + + @@ -7239,6 +8076,48 @@ limitations under the License. + + + + A list of TLS named groups (formerly known as curves) for + this cipher suite. These groups define the parameters for + key exchange algorithms like ECDHE. + + + + + + + + The name of the TLS group. + Example values: x25519, ffdhe2048 + + + + + + + + + + A list of signature schemes supported for cipher suite. + These schemes specify the algorithms used for digital + signatures in TLS handshakes and certificate verification. + + + + + + + + The name of the TLS signature scheme. + Example values: ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256, ed25519 + + + + + + @@ -7254,33 +8133,127 @@ limitations under the License. - + Transform Type 1: encryption algorithms + + EITHER a detailed description (PREFERRED) + OR a single string representing a "bom:refType" (DEPRECATED This will be removed in a future version.) + + + + + + A name for the encryption method. + Example: ENCR_AES_GCM_16 + + + + + + + The key length of the encryption algorithm. + + + + + + + The bom-ref to algorithm cryptographic asset. + + + + + - + Transform Type 2: pseudorandom functions + + EITHER a detailed description (PREFERRED) + OR a single string representing a "bom:refType" (DEPRECATED This will be removed in a future version.) + + + + + + A name for the pseudorandom function. + Example: PRF_HMAC_SHA2_256 + + + + + + + The bom-ref to algorithm cryptographic asset. + + + + + - + Transform Type 3: integrity algorithms + + EITHER a detailed description (PREFERRED) + OR a single string representing a "bom:refType" (DEPRECATED This will be removed in a future version.) + + + + + + A name for the integrity algorithm. + Example: AUTH_HMAC_SHA2_256_128 + + + + + + + The bom-ref to algorithm cryptographic asset. + + + + + - + Transform Type 4: Key Exchange Method (KE) per RFC9370, formerly called Diffie-Hellman Group (D-H) + + EITHER a detailed description (PREFERRED) + OR a single string representing a "bom:refType" (DEPRECATED This will be removed in a future version.) + + + + + + A group identifier for the key exchange algorithm. + + + + + + + The bom-ref to algorithm cryptographic asset. + + + + + @@ -7289,16 +8262,42 @@ limitations under the License. - + IKEv2 Authentication method + + EITHER a detailed description (PREFERRED) + OR a single string representing a "bom:refType" (DEPRECATED This will be removed in a future version.) + + + + + + A name for the authentication method. + + + + + + + The bom-ref to algorithm cryptographic asset. + + + + + + + + A protocol-related cryptographic assets + + @@ -7348,8 +8347,8 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere in the BOM. - Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the object elsewhere in the BOM. + Every `bom-ref` must be unique within the BOM. @@ -7615,8 +8614,8 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere - in the BOM. Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the object elsewhere + in the BOM. Every `bom-ref` must be unique within the BOM. @@ -7686,7 +8685,7 @@ limitations under the License. - An optional way to include textual or encoded data. + A way to include textual or encoded data. @@ -7722,7 +8721,7 @@ limitations under the License. - The optional date and time (timestamp) when the evidence is no longer valid. + The date and time (timestamp) when the evidence is no longer valid. @@ -7746,8 +8745,8 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere - in the BOM. Every bom-ref MUST be unique within the BOM. + A identifier which can be used to reference the object elsewhere + in the BOM. Every `bom-ref` must be unique within the BOM. @@ -7810,6 +8809,12 @@ limitations under the License. + + + A concise statement affirmed by an individual regarding all declarations, often used for third-party auditor acceptance or recipient acknowledgment. + It includes a list of authorized signatories who assert the validity of the document on behalf of the organization. + + @@ -7901,6 +8906,16 @@ limitations under the License. + + + + + + + + + + @@ -8003,7 +9018,7 @@ limitations under the License. - + @@ -8022,7 +9037,7 @@ limitations under the License. - The optional `bom-ref` to a parent requirement. This establishes a hierarchy of requirements. Top-level requirements must not define a parent. Only child requirements should define parents. + The `bom-ref` to a parent requirement. This establishes a hierarchy of requirements. Top-level requirements must not define a parent. Only child requirements should define parents. @@ -8033,7 +9048,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -8046,8 +9061,8 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere - in the BOM. Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the object elsewhere + in the BOM. Every `bom-ref` must be unique within the BOM. @@ -8110,8 +9125,8 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere - in the BOM. Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the object elsewhere + in the BOM. Every `bom-ref` must be unique within the BOM. @@ -8143,8 +9158,8 @@ limitations under the License. - An optional identifier which can be used to reference the object elsewhere - in the BOM. Every bom-ref MUST be unique within the BOM. + An identifier which can be used to reference the object elsewhere + in the BOM. Every `bom-ref` must be unique within the BOM. @@ -8172,6 +9187,444 @@ limitations under the License. + + + + A patent family is a group of related patent applications or granted patents that cover the same or similar invention. These patents are filed in multiple jurisdictions to protect the invention across different regions or countries. A patent family typically includes patents that share a common priority date, originating from the same initial application, and may vary slightly in scope or claims to comply with regional legal frameworks. Fields align with WIPO ST.96 standards where applicable. + + + + + + The unique identifier for the patent family, aligned with the `id` attribute in WIPO ST.96 v8.0's `PatentFamilyType`. Refer to [PatentFamilyType in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/PatentFamilyType.xsd). + + + + + + A collection of patents or applications that belong to this family, each identified by a `bom-ref` pointing to a patent object defined elsewhere in the BOM. + + + + + + + + + + External references provide a way to document systems, sites, and information that may be relevant but are not included with the BOM. They may also establish specific relationships within or external to the BOM. + + + + + + + An identifier which can be used to reference the object elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + + + + User-defined attributes may be used on this element as long as they + do not have the same name as an existing attribute used by the schema. + + + + + + + + A patent is a legal instrument, granted by an authority, that confers certain rights over an invention for a specified period, contingent on public disclosure and adherence to relevant legal requirements. The summary information in this object is aligned with [WIPO ST.96](https://www.wipo.int/standards/en/st96/) principles where applicable. + + + + + + The unique number assigned to the granted patent by the issuing authority. Aligned with `PatentNumber` in WIPO ST.96. Refer to [PatentNumber in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/PatentNumber.xsd). + + + + + + + + This is the number assigned to a patent application once it is published. Patent applications are generally published 18 months after filing (unless an applicant requests non-publication). This number is distinct from the application number. + + Purpose: Identifies the publicly available version of the application. + + Format: Varies by jurisdiction, often similar to application numbers but includes an additional suffix indicating publication. + + Example: + - US: US20240000123A1 (indicates the first publication of application US20240000123) + - Europe: EP23123456A1 (first publication of European application EP23123456). + + WIPO ST.96 v8.0: + - Publication Number field: https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/PublicationNumber.xsd + + + + + The title of the patent, summarising the invention it protects. Aligned with `InventionTitle` in WIPO ST.96. Refer to [InventionTitle in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/InventionTitle.xsd). + + + + + A brief summary of the invention described in the patent. Aligned with `Abstract` and `P` in WIPO ST.96. Refer to [Abstract in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/Abstract.xsd). + + + + + The date the patent application was filed with the jurisdiction. Aligned with `FilingDate` in WIPO ST.96. Refer to [FilingDate in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/FilingDate.xsd). + + + + + The date the patent was granted by the jurisdiction. Aligned with `GrantDate` in WIPO ST.96. Refer to [GrantDate in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Patent/GrantDate.xsd). + + + + + The date the patent expires. Derived from grant or filing date according to jurisdiction-specific rules. + + + + + Indicates the current legal status of the patent or patent application, based on the WIPO ST.27 standard. This status reflects administrative, procedural, or legal events. Values include both active and inactive states and are useful for determining enforceability, procedural history, and maintenance status. + + + + + Organisations or individuals to whom the patent rights are assigned. Supports joint ownership. + + + + + External references provide a way to document systems, sites, and information that may be relevant but are not included with the BOM. They may also establish specific relationships within or external to the BOM. + + + + + + + An identifier which can be used to reference the object elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + + + + User-defined attributes may be used on this element as long as they + do not have the same name as an existing attribute used by the schema. + + + + + + + + An assertion linking a patent or patent family to a component or service. This allows expression of ownership, licensing, third-party claims, and other legal relationships. + + + + + + The type of assertion being made (e.g. ownership, license, third-party-claim, etc.). + + + + + A list of references (`bom-ref`) linking to patents or families associated with this assertion. + + + + + + + + + + The organisation, individual, or BOM reference asserting the patent claim. + + + + + + + + + + + + Additional clarifications regarding the assertion, such as geographic or temporal constraints. + + + + + + + An identifier which can be used to reference the object elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + + + + User-defined attributes may be used on this element as long as they + do not have the same name as an existing attribute used by the schema. + + + + + + + + The legal status of the patent, reflecting various administrative or judicial states a patent or application may be in. Aligned with concepts in WIPO ST.27. + + + + + The patent application has been filed but not yet examined or granted. + + + The patent application has been examined and a patent has been issued. + + + The patent has been declared invalid through a legal or administrative process. + + + The patent has reached the end of its enforceable term. + + + The patent is no longer in force due to non-payment of maintenance fees or other requirements. + + + The patent application was voluntarily withdrawn by the applicant. + + + The patent application was abandoned, often due to lack of action or response. + + + Processing of the patent application has been temporarily halted. + + + A previously abandoned or lapsed patent has been reinstated. + + + The patent application or granted patent is under formal opposition proceedings. + + + The patent or application has been officially terminated. + + + The patent has been invalidated, either in part or in full. + + + The granted patent is active and enforceable. + + + + + + + + Specifies the type of assertion made about a patent or patent family. Enables documentation of legal, ownership, or usage-related claims. + + + + + The manufacturer asserts ownership of the patent or patent family. + + + The manufacturer asserts they have a license to use the patent or patent family. + + + A third party has asserted a claim or potential infringement against the manufacturer’s component or service. + + + The patent is part of a standard essential patent (SEP) portfolio relevant to the component or service. + + + The manufacturer asserts the patent or patent family as prior art that invalidates another patent or claim. + + + The manufacturer asserts exclusive rights granted through a licensing agreement. + + + The manufacturer asserts they will not enforce the patent or patent family against certain uses or users. + + + The patent or patent family is being used under a research or evaluation license. + + + + + + + + + + + + + + The jurisdiction or patent office where the priority application was filed, specified using WIPO ST.3 codes. Aligned with `IPOfficeCode` in ST.96. Refer to [IPOfficeCode in ST.96](https://www.wipo.int/standards/XMLSchema/ST96/V8_0/Common/IPOfficeCode.xsd). + + + + + + + + + + + The date the priority application was filed. Aligned with `FilingDate` in WIPO ST.96. + + + + + + + + + The priorityApplication contains the essential data necessary to identify and reference an earlier patent filing for priority rights. In line with WIPO ST.96 guidelines, it includes the jurisdiction (office code), application number, and filing date-the three key elements that uniquely specify the priority application in a global patent context. + + + + + + + + + + + + + + + Details a specific attribution of data within the BOM to a contributing entity or process. + + + + + + + + + + Details a specific attribution of data within the BOM to a contributing entity or process. + + + + + + Exactly one of the "pointers" or "expressions" elements must be present. + + + + + + + One or more JSON Pointers(https://datatracker.ietf.org/doc/html/rfc6901) identifying the BOM fields to which the attribution applies. + + + + + + + + A JSON Pointer(https://datatracker.ietf.org/doc/html/rfc6901) identifying the BOM field to which the attribution applies. + Users of other serialisation formats (e.g. XML) shall use the JSON Pointer format to ensure consistent field referencing across representations. + + + + + + + + + + One or more path expressions used to locate values within a BOM. + + + + + + + + Specifies a path expression used to locate a value within a BOM. The expression syntax shall conform to the format of the BOM's serialisation. + Use [JSONPath](https://datatracker.ietf.org/doc/html/rfc9535) for JSON, [XPath](https://www.w3.org/TR/xpath/) for XML, and default to JSONPath for Protocol Buffers unless otherwise specified. + Implementers shall ensure the expression is valid within the context of the applicable serialisation format. + + + + + + + + + + + The date and time when the attribution was made or the information was supplied. + + + + + + + The `bom-ref` of an object, such as a component, service, tool, organisational entity, or person that supplied the cited information. + At least one of the "attributedTo" or "process" elements must be present. + + + + + + + The `bom-ref` to a process (such as a formula, workflow, task, or step) defined in the `formulation` section that executed or generated the attributed data. + At least one of the "attributedTo" or "process" elements must be present. + + + + + + + An description or comment about the context or quality of the data attribution. + + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + + An identifier which can be used to reference the object elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + + + + User-defined attributes may be used on this element as long as they + do not have the same name as an existing attribute used by the schema. + + + + @@ -8213,7 +9666,7 @@ limitations under the License. without having to use additional namespaces or create extensions. Property names of interest to the general public are encouraged to be registered in the CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. - Formal registration is OPTIONAL. + Formal registration is optional. @@ -8232,10 +9685,13 @@ limitations under the License. - Describes how a component or service was manufactured or deployed. This is - achieved through the use of formulas, workflows, tasks, and steps, which declare the precise - steps to reproduce along with the observed formulas describing the steps which transpired - in the manufacturing process. + Describes the formulation of any referencable object within the BOM, + including components, services, metadata, declarations, or the BOM itself. This may + encompass how the object was created, assembled, deployed, tested, certified, or otherwise + brought into its present form. Common examples include software build pipelines, + deployment processes, AI/ML model training, cryptographic key generation or certification, + and third-party audits. Processes are modeled using declared and observed formulas, + composed of workflows, tasks, and individual steps. @@ -8253,6 +9709,11 @@ limitations under the License. + + + A collection of attributions indicating which entity supplied information for specific fields within the BOM. + + @@ -8272,8 +9733,8 @@ limitations under the License. Every BOM generated SHOULD have a unique serial number, even if the contents of - the BOM have not changed over time. If specified, the serial number MUST conform to RFC-4122. - Use of serial numbers are RECOMMENDED. + the BOM have not changed over time. If specified, the serial number must conform to RFC-4122. + Use of serial numbers are recommended. diff --git a/schema/cyclonedx/spdx.schema.json b/schema/cyclonedx/spdx.schema.json index 1e49a6d9e..092202126 100644 --- a/schema/cyclonedx/spdx.schema.json +++ b/schema/cyclonedx/spdx.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://cyclonedx.org/schema/spdx.schema.json", - "$comment": "v1.0-3.27.0", + "$comment": "v1.1-3.28.0", "type": "string", "enum": [ "0BSD", @@ -14,6 +14,7 @@ "Adobe-Glyph", "Adobe-Utopia", "ADSL", + "Advanced-Cryptics-Dictionary", "AFL-1.1", "AFL-1.2", "AFL-2.0", @@ -27,6 +28,7 @@ "AGPL-3.0-only", "AGPL-3.0-or-later", "Aladdin", + "ALGLIB-Documentation", "AMD-newlib", "AMDPLPA", "AML", @@ -68,6 +70,7 @@ "BlueOak-1.0.0", "Boehm-GC", "Boehm-GC-without-fee", + "BOLA-1.1", "Borceux", "Brian-Gladman-2-Clause", "Brian-Gladman-3-Clause", @@ -94,6 +97,7 @@ "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause-Open-MPI", "BSD-3-Clause-Sun", + "BSD-3-Clause-Tso", "BSD-4-Clause", "BSD-4-Clause-Shortened", "BSD-4-Clause-UC", @@ -102,12 +106,14 @@ "BSD-Advertising-Acknowledgement", "BSD-Attribution-HPND-disclaimer", "BSD-Inferno-Nettverk", + "BSD-Mark-Modifications", "BSD-Protection", "BSD-Source-beginning-file", "BSD-Source-Code", "BSD-Systemics", "BSD-Systemics-W3Works", "BSL-1.0", + "Buddy", "BUSL-1.1", "bzip2-1.0.5", "bzip2-1.0.6", @@ -116,6 +122,7 @@ "CAL-1.0-Combined-Work-Exception", "Caldera", "Caldera-no-preamble", + "CAPEC-tou", "Catharon", "CATOSL-1.1", "CC-BY-1.0", @@ -245,6 +252,9 @@ "EPL-1.0", "EPL-2.0", "ErlPL-1.1", + "ESA-PL-permissive-2.4", + "ESA-PL-strong-copyleft-2.4", + "ESA-PL-weak-copyleft-2.4", "etalab-2.0", "EUDatagrid", "EUPL-1.0", @@ -350,11 +360,14 @@ "HPND-sell-MIT-disclaimer-xserver", "HPND-sell-regexpr", "HPND-sell-variant", + "HPND-sell-variant-critical-systems", "HPND-sell-variant-MIT-disclaimer", "HPND-sell-variant-MIT-disclaimer-rev", + "HPND-SMC", "HPND-UC", "HPND-UC-export-US", "HTMLTIDY", + "hyphen-bulgarian", "IBM-pibs", "ICU", "IEC-Code-Components-EULA", @@ -373,6 +386,7 @@ "IPL-1.0", "ISC", "ISC-Veillard", + "ISO-permission", "Jam", "JasPer-2.0", "jove", @@ -450,10 +464,12 @@ "MIT-Khronos-old", "MIT-Modern-Variant", "MIT-open-group", + "MIT-STK", "MIT-testregex", "MIT-Wu", "MITNFA", "MMIXware", + "MMPL-1.0.1", "Motosoto", "MPEG-SSG", "mpi-permissive", @@ -487,6 +503,7 @@ "NICTA-1.0", "NIST-PD", "NIST-PD-fallback", + "NIST-PD-TNT", "NIST-Software", "NLOD-1.0", "NLOD-2.0", @@ -540,6 +557,7 @@ "OLDAP-2.8", "OLFL-1.3", "OML", + "OpenMDW-1.0", "OpenPBS-2.3", "OpenSSL", "OpenSSL-standalone", @@ -547,13 +565,16 @@ "OPL-1.0", "OPL-UK-3.0", "OPUBL-1.0", + "OSC-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1", "OSL-3.0", + "OSSP", "PADL", + "ParaType-Free-Font-1.3", "Parity-6.0.0", "Parity-7.0.0", "PDDL-1.0", @@ -598,6 +619,7 @@ "SGI-B-1.1", "SGI-B-2.0", "SGI-OpenGL", + "SGMLUG-PM", "SGP4", "SHL-0.5", "SHL-0.51", @@ -635,6 +657,7 @@ "TAPR-OHL-1.0", "TCL", "TCP-wrappers", + "TekHVC", "TermReadKey", "TGPPL-1.0", "ThirdEye", @@ -662,9 +685,11 @@ "Unlicense", "Unlicense-libtelnet", "Unlicense-libwhirlpool", + "UnRAR", "UPL-1.0", "URT-RLE", "Vim", + "Vixie-Cron", "VOSTROM", "VSL-1.0", "W3C", @@ -673,12 +698,15 @@ "w3m", "Watcom-1.0", "Widget-Workshop", + "WordNet", "Wsuipa", + "WTFNMFPL", "WTFPL", "wwl", "wxWindows", "X11", "X11-distribute-modifications-variant", + "X11-no-permit-persons", "X11-swapped", "Xdebug-1.03", "Xerox", @@ -716,6 +744,7 @@ "Bootloader-exception", "CGAL-linking-exception", "Classpath-exception-2.0", + "Classpath-exception-2.0-short", "CLISP-exception-2.0", "cryptsetup-OpenSSL-exception", "Digia-Qt-LGPL-exception-1.1", @@ -746,6 +775,7 @@ "i2p-gpl-java-exception", "Independent-modules-exception", "KiCad-libraries-exception", + "kvirc-openssl-exception", "LGPL-3.0-linking-exception", "libpri-OpenH323-exception", "Libtool-exception", @@ -769,9 +799,12 @@ "Qwt-exception-1.0", "romic-exception", "RRDtool-FLOSS-exception-2.0", + "rsync-linking-exception", "SANE-exception", "SHL-2.0", "SHL-2.1", + "Simple-Library-Usage-exception", + "sqlitestudio-OpenSSL-exception", "stunnel-exception", "SWI-exception", "Swift-exception", @@ -782,5 +815,818 @@ "vsftpd-openssl-exception", "WxWindows-exception-3.1", "x11vnc-openssl-exception" - ] -} + ], + "meta:enum": { + "0BSD": "BSD Zero Clause License", + "3D-Slicer-1.0": "3D Slicer License v1.0", + "AAL": "Attribution Assurance License", + "Abstyles": "Abstyles License", + "AdaCore-doc": "AdaCore Doc License", + "Adobe-2006": "Adobe Systems Incorporated Source Code License Agreement", + "Adobe-Display-PostScript": "Adobe Display PostScript License", + "Adobe-Glyph": "Adobe Glyph List License", + "Adobe-Utopia": "Adobe Utopia Font License", + "ADSL": "Amazon Digital Services License", + "Advanced-Cryptics-Dictionary": "Advanced Cryptics Dictionary License", + "AFL-1.1": "Academic Free License v1.1", + "AFL-1.2": "Academic Free License v1.2", + "AFL-2.0": "Academic Free License v2.0", + "AFL-2.1": "Academic Free License v2.1", + "AFL-3.0": "Academic Free License v3.0", + "Afmparse": "Afmparse License", + "AGPL-1.0": "Affero General Public License v1.0", + "AGPL-1.0-only": "Affero General Public License v1.0 only", + "AGPL-1.0-or-later": "Affero General Public License v1.0 or later", + "AGPL-3.0": "GNU Affero General Public License v3.0", + "AGPL-3.0-only": "GNU Affero General Public License v3.0 only", + "AGPL-3.0-or-later": "GNU Affero General Public License v3.0 or later", + "Aladdin": "Aladdin Free Public License", + "ALGLIB-Documentation": "ALGLIB Documentation License", + "AMD-newlib": "AMD newlib License", + "AMDPLPA": "AMD's plpa_map.c License", + "AML": "Apple MIT License", + "AML-glslang": "AML glslang variant License", + "AMPAS": "Academy of Motion Picture Arts and Sciences BSD", + "ANTLR-PD": "ANTLR Software Rights Notice", + "ANTLR-PD-fallback": "ANTLR Software Rights Notice with license fallback", + "any-OSI": "Any OSI License", + "any-OSI-perl-modules": "Any OSI License - Perl Modules", + "Apache-1.0": "Apache License 1.0", + "Apache-1.1": "Apache License 1.1", + "Apache-2.0": "Apache License 2.0", + "APAFML": "Adobe Postscript AFM License", + "APL-1.0": "Adaptive Public License 1.0", + "App-s2p": "App::s2p License", + "APSL-1.0": "Apple Public Source License 1.0", + "APSL-1.1": "Apple Public Source License 1.1", + "APSL-1.2": "Apple Public Source License 1.2", + "APSL-2.0": "Apple Public Source License 2.0", + "Arphic-1999": "Arphic Public License", + "Artistic-1.0": "Artistic License 1.0", + "Artistic-1.0-cl8": "Artistic License 1.0 w\/clause 8", + "Artistic-1.0-Perl": "Artistic License 1.0 (Perl)", + "Artistic-2.0": "Artistic License 2.0", + "Artistic-dist": "Artistic License 1.0 (dist)", + "Aspell-RU": "Aspell Russian License", + "ASWF-Digital-Assets-1.0": "ASWF Digital Assets License version 1.0", + "ASWF-Digital-Assets-1.1": "ASWF Digital Assets License 1.1", + "Baekmuk": "Baekmuk License", + "Bahyph": "Bahyph License", + "Barr": "Barr License", + "bcrypt-Solar-Designer": "bcrypt Solar Designer License", + "Beerware": "Beerware License", + "Bitstream-Charter": "Bitstream Charter Font License", + "Bitstream-Vera": "Bitstream Vera Font License", + "BitTorrent-1.0": "BitTorrent Open Source License v1.0", + "BitTorrent-1.1": "BitTorrent Open Source License v1.1", + "blessing": "SQLite Blessing", + "BlueOak-1.0.0": "Blue Oak Model License 1.0.0", + "Boehm-GC": "Boehm-Demers-Weiser GC License", + "Boehm-GC-without-fee": "Boehm-Demers-Weiser GC License (without fee)", + "BOLA-1.1": "Buena Onda License Agreement v1.1", + "Borceux": "Borceux license", + "Brian-Gladman-2-Clause": "Brian Gladman 2-Clause License", + "Brian-Gladman-3-Clause": "Brian Gladman 3-Clause License", + "BSD-1-Clause": "BSD 1-Clause License", + "BSD-2-Clause": "BSD 2-Clause \"Simplified\" License", + "BSD-2-Clause-Darwin": "BSD 2-Clause - Ian Darwin variant", + "BSD-2-Clause-first-lines": "BSD 2-Clause - first lines requirement", + "BSD-2-Clause-FreeBSD": "BSD 2-Clause FreeBSD License", + "BSD-2-Clause-NetBSD": "BSD 2-Clause NetBSD License", + "BSD-2-Clause-Patent": "BSD-2-Clause Plus Patent License", + "BSD-2-Clause-pkgconf-disclaimer": "BSD 2-Clause pkgconf disclaimer variant", + "BSD-2-Clause-Views": "BSD 2-Clause with views sentence", + "BSD-3-Clause": "BSD 3-Clause \"New\" or \"Revised\" License", + "BSD-3-Clause-acpica": "BSD 3-Clause acpica variant", + "BSD-3-Clause-Attribution": "BSD with attribution", + "BSD-3-Clause-Clear": "BSD 3-Clause Clear License", + "BSD-3-Clause-flex": "BSD 3-Clause Flex variant", + "BSD-3-Clause-HP": "Hewlett-Packard BSD variant license", + "BSD-3-Clause-LBNL": "Lawrence Berkeley National Labs BSD variant license", + "BSD-3-Clause-Modification": "BSD 3-Clause Modification", + "BSD-3-Clause-No-Military-License": "BSD 3-Clause No Military License", + "BSD-3-Clause-No-Nuclear-License": "BSD 3-Clause No Nuclear License", + "BSD-3-Clause-No-Nuclear-License-2014": "BSD 3-Clause No Nuclear License 2014", + "BSD-3-Clause-No-Nuclear-Warranty": "BSD 3-Clause No Nuclear Warranty", + "BSD-3-Clause-Open-MPI": "BSD 3-Clause Open MPI variant", + "BSD-3-Clause-Sun": "BSD 3-Clause Sun Microsystems", + "BSD-3-Clause-Tso": "BSD 3-Clause Tso variant", + "BSD-4-Clause": "BSD 4-Clause \"Original\" or \"Old\" License", + "BSD-4-Clause-Shortened": "BSD 4 Clause Shortened", + "BSD-4-Clause-UC": "BSD-4-Clause (University of California-Specific)", + "BSD-4.3RENO": "BSD 4.3 RENO License", + "BSD-4.3TAHOE": "BSD 4.3 TAHOE License", + "BSD-Advertising-Acknowledgement": "BSD Advertising Acknowledgement License", + "BSD-Attribution-HPND-disclaimer": "BSD with Attribution and HPND disclaimer", + "BSD-Inferno-Nettverk": "BSD-Inferno-Nettverk", + "BSD-Mark-Modifications": "BSD Mark Modifications License", + "BSD-Protection": "BSD Protection License", + "BSD-Source-beginning-file": "BSD Source Code Attribution - beginning of file variant", + "BSD-Source-Code": "BSD Source Code Attribution", + "BSD-Systemics": "Systemics BSD variant license", + "BSD-Systemics-W3Works": "Systemics W3Works BSD variant license", + "BSL-1.0": "Boost Software License 1.0", + "Buddy": "Buddy License", + "BUSL-1.1": "Business Source License 1.1", + "bzip2-1.0.5": "bzip2 and libbzip2 License v1.0.5", + "bzip2-1.0.6": "bzip2 and libbzip2 License v1.0.6", + "C-UDA-1.0": "Computational Use of Data Agreement v1.0", + "CAL-1.0": "Cryptographic Autonomy License 1.0", + "CAL-1.0-Combined-Work-Exception": "Cryptographic Autonomy License 1.0 (Combined Work Exception)", + "Caldera": "Caldera License", + "Caldera-no-preamble": "Caldera License (without preamble)", + "CAPEC-tou": "Common Attack Pattern Enumeration and Classification License", + "Catharon": "Catharon License", + "CATOSL-1.1": "Computer Associates Trusted Open Source License 1.1", + "CC-BY-1.0": "Creative Commons Attribution 1.0 Generic", + "CC-BY-2.0": "Creative Commons Attribution 2.0 Generic", + "CC-BY-2.5": "Creative Commons Attribution 2.5 Generic", + "CC-BY-2.5-AU": "Creative Commons Attribution 2.5 Australia", + "CC-BY-3.0": "Creative Commons Attribution 3.0 Unported", + "CC-BY-3.0-AT": "Creative Commons Attribution 3.0 Austria", + "CC-BY-3.0-AU": "Creative Commons Attribution 3.0 Australia", + "CC-BY-3.0-DE": "Creative Commons Attribution 3.0 Germany", + "CC-BY-3.0-IGO": "Creative Commons Attribution 3.0 IGO", + "CC-BY-3.0-NL": "Creative Commons Attribution 3.0 Netherlands", + "CC-BY-3.0-US": "Creative Commons Attribution 3.0 United States", + "CC-BY-4.0": "Creative Commons Attribution 4.0 International", + "CC-BY-NC-1.0": "Creative Commons Attribution Non Commercial 1.0 Generic", + "CC-BY-NC-2.0": "Creative Commons Attribution Non Commercial 2.0 Generic", + "CC-BY-NC-2.5": "Creative Commons Attribution Non Commercial 2.5 Generic", + "CC-BY-NC-3.0": "Creative Commons Attribution Non Commercial 3.0 Unported", + "CC-BY-NC-3.0-DE": "Creative Commons Attribution Non Commercial 3.0 Germany", + "CC-BY-NC-4.0": "Creative Commons Attribution Non Commercial 4.0 International", + "CC-BY-NC-ND-1.0": "Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic", + "CC-BY-NC-ND-2.0": "Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic", + "CC-BY-NC-ND-2.5": "Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic", + "CC-BY-NC-ND-3.0": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported", + "CC-BY-NC-ND-3.0-DE": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany", + "CC-BY-NC-ND-3.0-IGO": "Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO", + "CC-BY-NC-ND-4.0": "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", + "CC-BY-NC-SA-1.0": "Creative Commons Attribution Non Commercial Share Alike 1.0 Generic", + "CC-BY-NC-SA-2.0": "Creative Commons Attribution Non Commercial Share Alike 2.0 Generic", + "CC-BY-NC-SA-2.0-DE": "Creative Commons Attribution Non Commercial Share Alike 2.0 Germany", + "CC-BY-NC-SA-2.0-FR": "Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France", + "CC-BY-NC-SA-2.0-UK": "Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales", + "CC-BY-NC-SA-2.5": "Creative Commons Attribution Non Commercial Share Alike 2.5 Generic", + "CC-BY-NC-SA-3.0": "Creative Commons Attribution Non Commercial Share Alike 3.0 Unported", + "CC-BY-NC-SA-3.0-DE": "Creative Commons Attribution Non Commercial Share Alike 3.0 Germany", + "CC-BY-NC-SA-3.0-IGO": "Creative Commons Attribution Non Commercial Share Alike 3.0 IGO", + "CC-BY-NC-SA-4.0": "Creative Commons Attribution Non Commercial Share Alike 4.0 International", + "CC-BY-ND-1.0": "Creative Commons Attribution No Derivatives 1.0 Generic", + "CC-BY-ND-2.0": "Creative Commons Attribution No Derivatives 2.0 Generic", + "CC-BY-ND-2.5": "Creative Commons Attribution No Derivatives 2.5 Generic", + "CC-BY-ND-3.0": "Creative Commons Attribution No Derivatives 3.0 Unported", + "CC-BY-ND-3.0-DE": "Creative Commons Attribution No Derivatives 3.0 Germany", + "CC-BY-ND-4.0": "Creative Commons Attribution No Derivatives 4.0 International", + "CC-BY-SA-1.0": "Creative Commons Attribution Share Alike 1.0 Generic", + "CC-BY-SA-2.0": "Creative Commons Attribution Share Alike 2.0 Generic", + "CC-BY-SA-2.0-UK": "Creative Commons Attribution Share Alike 2.0 England and Wales", + "CC-BY-SA-2.1-JP": "Creative Commons Attribution Share Alike 2.1 Japan", + "CC-BY-SA-2.5": "Creative Commons Attribution Share Alike 2.5 Generic", + "CC-BY-SA-3.0": "Creative Commons Attribution Share Alike 3.0 Unported", + "CC-BY-SA-3.0-AT": "Creative Commons Attribution Share Alike 3.0 Austria", + "CC-BY-SA-3.0-DE": "Creative Commons Attribution Share Alike 3.0 Germany", + "CC-BY-SA-3.0-IGO": "Creative Commons Attribution-ShareAlike 3.0 IGO", + "CC-BY-SA-4.0": "Creative Commons Attribution Share Alike 4.0 International", + "CC-PDDC": "Creative Commons Public Domain Dedication and Certification", + "CC-PDM-1.0": "Creative Commons Public Domain Mark 1.0 Universal", + "CC-SA-1.0": "Creative Commons Share Alike 1.0 Generic", + "CC0-1.0": "Creative Commons Zero v1.0 Universal", + "CDDL-1.0": "Common Development and Distribution License 1.0", + "CDDL-1.1": "Common Development and Distribution License 1.1", + "CDL-1.0": "Common Documentation License 1.0", + "CDLA-Permissive-1.0": "Community Data License Agreement Permissive 1.0", + "CDLA-Permissive-2.0": "Community Data License Agreement Permissive 2.0", + "CDLA-Sharing-1.0": "Community Data License Agreement Sharing 1.0", + "CECILL-1.0": "CeCILL Free Software License Agreement v1.0", + "CECILL-1.1": "CeCILL Free Software License Agreement v1.1", + "CECILL-2.0": "CeCILL Free Software License Agreement v2.0", + "CECILL-2.1": "CeCILL Free Software License Agreement v2.1", + "CECILL-B": "CeCILL-B Free Software License Agreement", + "CECILL-C": "CeCILL-C Free Software License Agreement", + "CERN-OHL-1.1": "CERN Open Hardware Licence v1.1", + "CERN-OHL-1.2": "CERN Open Hardware Licence v1.2", + "CERN-OHL-P-2.0": "CERN Open Hardware Licence Version 2 - Permissive", + "CERN-OHL-S-2.0": "CERN Open Hardware Licence Version 2 - Strongly Reciprocal", + "CERN-OHL-W-2.0": "CERN Open Hardware Licence Version 2 - Weakly Reciprocal", + "CFITSIO": "CFITSIO License", + "check-cvs": "check-cvs License", + "checkmk": "Checkmk License", + "ClArtistic": "Clarified Artistic License", + "Clips": "Clips License", + "CMU-Mach": "CMU Mach License", + "CMU-Mach-nodoc": "CMU Mach - no notices-in-documentation variant", + "CNRI-Jython": "CNRI Jython License", + "CNRI-Python": "CNRI Python License", + "CNRI-Python-GPL-Compatible": "CNRI Python Open Source GPL Compatible License Agreement", + "COIL-1.0": "Copyfree Open Innovation License", + "Community-Spec-1.0": "Community Specification License 1.0", + "Condor-1.1": "Condor Public License v1.1", + "copyleft-next-0.3.0": "copyleft-next 0.3.0", + "copyleft-next-0.3.1": "copyleft-next 0.3.1", + "Cornell-Lossless-JPEG": "Cornell Lossless JPEG License", + "CPAL-1.0": "Common Public Attribution License 1.0", + "CPL-1.0": "Common Public License 1.0", + "CPOL-1.02": "Code Project Open License 1.02", + "Cronyx": "Cronyx License", + "Crossword": "Crossword License", + "CryptoSwift": "CryptoSwift License", + "CrystalStacker": "CrystalStacker License", + "CUA-OPL-1.0": "CUA Office Public License v1.0", + "Cube": "Cube License", + "curl": "curl License", + "cve-tou": "Common Vulnerability Enumeration ToU License", + "D-FSL-1.0": "Deutsche Freie Software Lizenz", + "DEC-3-Clause": "DEC 3-Clause License", + "diffmark": "diffmark license", + "DL-DE-BY-2.0": "Data licence Germany \u2013 attribution \u2013 version 2.0", + "DL-DE-ZERO-2.0": "Data licence Germany \u2013 zero \u2013 version 2.0", + "DOC": "DOC License", + "DocBook-DTD": "DocBook DTD License", + "DocBook-Schema": "DocBook Schema License", + "DocBook-Stylesheet": "DocBook Stylesheet License", + "DocBook-XML": "DocBook XML License", + "Dotseqn": "Dotseqn License", + "DRL-1.0": "Detection Rule License 1.0", + "DRL-1.1": "Detection Rule License 1.1", + "DSDP": "DSDP License", + "dtoa": "David M. Gay dtoa License", + "dvipdfm": "dvipdfm License", + "ECL-1.0": "Educational Community License v1.0", + "ECL-2.0": "Educational Community License v2.0", + "eCos-2.0": "eCos license version 2.0", + "EFL-1.0": "Eiffel Forum License v1.0", + "EFL-2.0": "Eiffel Forum License v2.0", + "eGenix": "eGenix.com Public License 1.1.0", + "Elastic-2.0": "Elastic License 2.0", + "Entessa": "Entessa Public License v1.0", + "EPICS": "EPICS Open License", + "EPL-1.0": "Eclipse Public License 1.0", + "EPL-2.0": "Eclipse Public License 2.0", + "ErlPL-1.1": "Erlang Public License v1.1", + "ESA-PL-permissive-2.4": "European Space Agency Public License \u2013 v2.4 \u2013 Permissive (Type 3)", + "ESA-PL-strong-copyleft-2.4": "European Space Agency Public License (ESA-PL) - V2.4 - Strong Copyleft (Type 1)", + "ESA-PL-weak-copyleft-2.4": "European Space Agency Public License \u2013 v2.4 \u2013 Weak Copyleft (Type 2)", + "etalab-2.0": "Etalab Open License 2.0", + "EUDatagrid": "EU DataGrid Software License", + "EUPL-1.0": "European Union Public License 1.0", + "EUPL-1.1": "European Union Public License 1.1", + "EUPL-1.2": "European Union Public License 1.2", + "Eurosym": "Eurosym License", + "Fair": "Fair License", + "FBM": "Fuzzy Bitmap License", + "FDK-AAC": "Fraunhofer FDK AAC Codec Library", + "Ferguson-Twofish": "Ferguson Twofish License", + "Frameworx-1.0": "Frameworx Open License 1.0", + "FreeBSD-DOC": "FreeBSD Documentation License", + "FreeImage": "FreeImage Public License v1.0", + "FSFAP": "FSF All Permissive License", + "FSFAP-no-warranty-disclaimer": "FSF All Permissive License (without Warranty)", + "FSFUL": "FSF Unlimited License", + "FSFULLR": "FSF Unlimited License (with License Retention)", + "FSFULLRSD": "FSF Unlimited License (with License Retention and Short Disclaimer)", + "FSFULLRWD": "FSF Unlimited License (With License Retention and Warranty Disclaimer)", + "FSL-1.1-ALv2": "Functional Source License, Version 1.1, ALv2 Future License", + "FSL-1.1-MIT": "Functional Source License, Version 1.1, MIT Future License", + "FTL": "Freetype Project License", + "Furuseth": "Furuseth License", + "fwlw": "fwlw License", + "Game-Programming-Gems": "Game Programming Gems License", + "GCR-docs": "Gnome GCR Documentation License", + "GD": "GD License", + "generic-xts": "Generic XTS License", + "GFDL-1.1": "GNU Free Documentation License v1.1", + "GFDL-1.1-invariants-only": "GNU Free Documentation License v1.1 only - invariants", + "GFDL-1.1-invariants-or-later": "GNU Free Documentation License v1.1 or later - invariants", + "GFDL-1.1-no-invariants-only": "GNU Free Documentation License v1.1 only - no invariants", + "GFDL-1.1-no-invariants-or-later": "GNU Free Documentation License v1.1 or later - no invariants", + "GFDL-1.1-only": "GNU Free Documentation License v1.1 only", + "GFDL-1.1-or-later": "GNU Free Documentation License v1.1 or later", + "GFDL-1.2": "GNU Free Documentation License v1.2", + "GFDL-1.2-invariants-only": "GNU Free Documentation License v1.2 only - invariants", + "GFDL-1.2-invariants-or-later": "GNU Free Documentation License v1.2 or later - invariants", + "GFDL-1.2-no-invariants-only": "GNU Free Documentation License v1.2 only - no invariants", + "GFDL-1.2-no-invariants-or-later": "GNU Free Documentation License v1.2 or later - no invariants", + "GFDL-1.2-only": "GNU Free Documentation License v1.2 only", + "GFDL-1.2-or-later": "GNU Free Documentation License v1.2 or later", + "GFDL-1.3": "GNU Free Documentation License v1.3", + "GFDL-1.3-invariants-only": "GNU Free Documentation License v1.3 only - invariants", + "GFDL-1.3-invariants-or-later": "GNU Free Documentation License v1.3 or later - invariants", + "GFDL-1.3-no-invariants-only": "GNU Free Documentation License v1.3 only - no invariants", + "GFDL-1.3-no-invariants-or-later": "GNU Free Documentation License v1.3 or later - no invariants", + "GFDL-1.3-only": "GNU Free Documentation License v1.3 only", + "GFDL-1.3-or-later": "GNU Free Documentation License v1.3 or later", + "Giftware": "Giftware License", + "GL2PS": "GL2PS License", + "Glide": "3dfx Glide License", + "Glulxe": "Glulxe License", + "GLWTPL": "Good Luck With That Public License", + "gnuplot": "gnuplot License", + "GPL-1.0": "GNU General Public License v1.0 only", + "GPL-1.0+": "GNU General Public License v1.0 or later", + "GPL-1.0-only": "GNU General Public License v1.0 only", + "GPL-1.0-or-later": "GNU General Public License v1.0 or later", + "GPL-2.0": "GNU General Public License v2.0 only", + "GPL-2.0+": "GNU General Public License v2.0 or later", + "GPL-2.0-only": "GNU General Public License v2.0 only", + "GPL-2.0-or-later": "GNU General Public License v2.0 or later", + "GPL-2.0-with-autoconf-exception": "GNU General Public License v2.0 w\/Autoconf exception", + "GPL-2.0-with-bison-exception": "GNU General Public License v2.0 w\/Bison exception", + "GPL-2.0-with-classpath-exception": "GNU General Public License v2.0 w\/Classpath exception", + "GPL-2.0-with-font-exception": "GNU General Public License v2.0 w\/Font exception", + "GPL-2.0-with-GCC-exception": "GNU General Public License v2.0 w\/GCC Runtime Library exception", + "GPL-3.0": "GNU General Public License v3.0 only", + "GPL-3.0+": "GNU General Public License v3.0 or later", + "GPL-3.0-only": "GNU General Public License v3.0 only", + "GPL-3.0-or-later": "GNU General Public License v3.0 or later", + "GPL-3.0-with-autoconf-exception": "GNU General Public License v3.0 w\/Autoconf exception", + "GPL-3.0-with-GCC-exception": "GNU General Public License v3.0 w\/GCC Runtime Library exception", + "Graphics-Gems": "Graphics Gems License", + "gSOAP-1.3b": "gSOAP Public License v1.3b", + "gtkbook": "gtkbook License", + "Gutmann": "Gutmann License", + "HaskellReport": "Haskell Language Report License", + "HDF5": "HDF5 License", + "hdparm": "hdparm License", + "HIDAPI": "HIDAPI License", + "Hippocratic-2.1": "Hippocratic License 2.1", + "HP-1986": "Hewlett-Packard 1986 License", + "HP-1989": "Hewlett-Packard 1989 License", + "HPND": "Historical Permission Notice and Disclaimer", + "HPND-DEC": "Historical Permission Notice and Disclaimer - DEC variant", + "HPND-doc": "Historical Permission Notice and Disclaimer - documentation variant", + "HPND-doc-sell": "Historical Permission Notice and Disclaimer - documentation sell variant", + "HPND-export-US": "HPND with US Government export control warning", + "HPND-export-US-acknowledgement": "HPND with US Government export control warning and acknowledgment", + "HPND-export-US-modify": "HPND with US Government export control warning and modification rqmt", + "HPND-export2-US": "HPND with US Government export control and 2 disclaimers", + "HPND-Fenneberg-Livingston": "Historical Permission Notice and Disclaimer - Fenneberg-Livingston variant", + "HPND-INRIA-IMAG": "Historical Permission Notice and Disclaimer - INRIA-IMAG variant", + "HPND-Intel": "Historical Permission Notice and Disclaimer - Intel variant", + "HPND-Kevlin-Henney": "Historical Permission Notice and Disclaimer - Kevlin Henney variant", + "HPND-Markus-Kuhn": "Historical Permission Notice and Disclaimer - Markus Kuhn variant", + "HPND-merchantability-variant": "Historical Permission Notice and Disclaimer - merchantability variant", + "HPND-MIT-disclaimer": "Historical Permission Notice and Disclaimer with MIT disclaimer", + "HPND-Netrek": "Historical Permission Notice and Disclaimer - Netrek variant", + "HPND-Pbmplus": "Historical Permission Notice and Disclaimer - Pbmplus variant", + "HPND-sell-MIT-disclaimer-xserver": "Historical Permission Notice and Disclaimer - sell xserver variant with MIT disclaimer", + "HPND-sell-regexpr": "Historical Permission Notice and Disclaimer - sell regexpr variant", + "HPND-sell-variant": "Historical Permission Notice and Disclaimer - sell variant", + "HPND-sell-variant-critical-systems": "HPND - sell variant with safety critical systems clause", + "HPND-sell-variant-MIT-disclaimer": "HPND sell variant with MIT disclaimer", + "HPND-sell-variant-MIT-disclaimer-rev": "HPND sell variant with MIT disclaimer - reverse", + "HPND-SMC": "Historical Permission Notice and Disclaimer - SMC variant", + "HPND-UC": "Historical Permission Notice and Disclaimer - University of California variant", + "HPND-UC-export-US": "Historical Permission Notice and Disclaimer - University of California, US export warning", + "HTMLTIDY": "HTML Tidy License", + "hyphen-bulgarian": "hyphen-bulgarian License", + "IBM-pibs": "IBM PowerPC Initialization and Boot Software", + "ICU": "ICU License", + "IEC-Code-Components-EULA": "IEC Code Components End-user licence agreement", + "IJG": "Independent JPEG Group License", + "IJG-short": "Independent JPEG Group License - short", + "ImageMagick": "ImageMagick License", + "iMatix": "iMatix Standard Function Library Agreement", + "Imlib2": "Imlib2 License", + "Info-ZIP": "Info-ZIP License", + "Inner-Net-2.0": "Inner Net License v2.0", + "InnoSetup": "Inno Setup License", + "Intel": "Intel Open Source License", + "Intel-ACPI": "Intel ACPI Software License Agreement", + "Interbase-1.0": "Interbase Public License v1.0", + "IPA": "IPA Font License", + "IPL-1.0": "IBM Public License v1.0", + "ISC": "ISC License", + "ISC-Veillard": "ISC Veillard variant", + "ISO-permission": "ISO permission notice", + "Jam": "Jam License", + "JasPer-2.0": "JasPer License", + "jove": "Jove License", + "JPL-image": "JPL Image Use Policy", + "JPNIC": "Japan Network Information Center License", + "JSON": "JSON License", + "Kastrup": "Kastrup License", + "Kazlib": "Kazlib License", + "Knuth-CTAN": "Knuth CTAN License", + "LAL-1.2": "Licence Art Libre 1.2", + "LAL-1.3": "Licence Art Libre 1.3", + "Latex2e": "Latex2e License", + "Latex2e-translated-notice": "Latex2e with translated notice permission", + "Leptonica": "Leptonica License", + "LGPL-2.0": "GNU Library General Public License v2 only", + "LGPL-2.0+": "GNU Library General Public License v2 or later", + "LGPL-2.0-only": "GNU Library General Public License v2 only", + "LGPL-2.0-or-later": "GNU Library General Public License v2 or later", + "LGPL-2.1": "GNU Lesser General Public License v2.1 only", + "LGPL-2.1+": "GNU Lesser General Public License v2.1 or later", + "LGPL-2.1-only": "GNU Lesser General Public License v2.1 only", + "LGPL-2.1-or-later": "GNU Lesser General Public License v2.1 or later", + "LGPL-3.0": "GNU Lesser General Public License v3.0 only", + "LGPL-3.0+": "GNU Lesser General Public License v3.0 or later", + "LGPL-3.0-only": "GNU Lesser General Public License v3.0 only", + "LGPL-3.0-or-later": "GNU Lesser General Public License v3.0 or later", + "LGPLLR": "Lesser General Public License For Linguistic Resources", + "Libpng": "libpng License", + "libpng-1.6.35": "PNG Reference Library License v1 (for libpng 0.5 through 1.6.35)", + "libpng-2.0": "PNG Reference Library version 2", + "libselinux-1.0": "libselinux public domain notice", + "libtiff": "libtiff License", + "libutil-David-Nugent": "libutil David Nugent License", + "LiLiQ-P-1.1": "Licence Libre du Qu\u00E9bec \u2013 Permissive version 1.1", + "LiLiQ-R-1.1": "Licence Libre du Qu\u00E9bec \u2013 R\u00E9ciprocit\u00E9 version 1.1", + "LiLiQ-Rplus-1.1": "Licence Libre du Qu\u00E9bec \u2013 R\u00E9ciprocit\u00E9 forte version 1.1", + "Linux-man-pages-1-para": "Linux man-pages - 1 paragraph", + "Linux-man-pages-copyleft": "Linux man-pages Copyleft", + "Linux-man-pages-copyleft-2-para": "Linux man-pages Copyleft - 2 paragraphs", + "Linux-man-pages-copyleft-var": "Linux man-pages Copyleft Variant", + "Linux-OpenIB": "Linux Kernel Variant of OpenIB.org license", + "LOOP": "Common Lisp LOOP License", + "LPD-document": "LPD Documentation License", + "LPL-1.0": "Lucent Public License Version 1.0", + "LPL-1.02": "Lucent Public License v1.02", + "LPPL-1.0": "LaTeX Project Public License v1.0", + "LPPL-1.1": "LaTeX Project Public License v1.1", + "LPPL-1.2": "LaTeX Project Public License v1.2", + "LPPL-1.3a": "LaTeX Project Public License v1.3a", + "LPPL-1.3c": "LaTeX Project Public License v1.3c", + "lsof": "lsof License", + "Lucida-Bitmap-Fonts": "Lucida Bitmap Fonts License", + "LZMA-SDK-9.11-to-9.20": "LZMA SDK License (versions 9.11 to 9.20)", + "LZMA-SDK-9.22": "LZMA SDK License (versions 9.22 and beyond)", + "Mackerras-3-Clause": "Mackerras 3-Clause License", + "Mackerras-3-Clause-acknowledgment": "Mackerras 3-Clause - acknowledgment variant", + "magaz": "magaz License", + "mailprio": "mailprio License", + "MakeIndex": "MakeIndex License", + "man2html": "man2html License", + "Martin-Birgmeier": "Martin Birgmeier License", + "McPhee-slideshow": "McPhee Slideshow License", + "metamail": "metamail License", + "Minpack": "Minpack License", + "MIPS": "MIPS License", + "MirOS": "The MirOS Licence", + "MIT": "MIT License", + "MIT-0": "MIT No Attribution", + "MIT-advertising": "Enlightenment License (e16)", + "MIT-Click": "MIT Click License", + "MIT-CMU": "CMU License", + "MIT-enna": "enna License", + "MIT-feh": "feh License", + "MIT-Festival": "MIT Festival Variant", + "MIT-Khronos-old": "MIT Khronos - old variant", + "MIT-Modern-Variant": "MIT License Modern Variant", + "MIT-open-group": "MIT Open Group variant", + "MIT-STK": "MIT-STK License", + "MIT-testregex": "MIT testregex Variant", + "MIT-Wu": "MIT Tom Wu Variant", + "MITNFA": "MIT +no-false-attribs license", + "MMIXware": "MMIXware License", + "MMPL-1.0.1": "Minecraft Mod Public License v1.0.1", + "Motosoto": "Motosoto License", + "MPEG-SSG": "MPEG Software Simulation", + "mpi-permissive": "mpi Permissive License", + "mpich2": "mpich2 License", + "MPL-1.0": "Mozilla Public License 1.0", + "MPL-1.1": "Mozilla Public License 1.1", + "MPL-2.0": "Mozilla Public License 2.0", + "MPL-2.0-no-copyleft-exception": "Mozilla Public License 2.0 (no copyleft exception)", + "mplus": "mplus Font License", + "MS-LPL": "Microsoft Limited Public License", + "MS-PL": "Microsoft Public License", + "MS-RL": "Microsoft Reciprocal License", + "MTLL": "Matrix Template Library License", + "MulanPSL-1.0": "Mulan Permissive Software License, Version 1", + "MulanPSL-2.0": "Mulan Permissive Software License, Version 2", + "Multics": "Multics License", + "Mup": "Mup License", + "NAIST-2003": "Nara Institute of Science and Technology License (2003)", + "NASA-1.3": "NASA Open Source Agreement 1.3", + "Naumen": "Naumen Public License", + "NBPL-1.0": "Net Boolean Public License v1", + "NCBI-PD": "NCBI Public Domain Notice", + "NCGL-UK-2.0": "Non-Commercial Government Licence", + "NCL": "NCL Source Code License", + "NCSA": "University of Illinois\/NCSA Open Source License", + "Net-SNMP": "Net-SNMP License", + "NetCDF": "NetCDF license", + "Newsletr": "Newsletr License", + "NGPL": "Nethack General Public License", + "ngrep": "ngrep License", + "NICTA-1.0": "NICTA Public Software License, Version 1.0", + "NIST-PD": "NIST Public Domain Notice", + "NIST-PD-fallback": "NIST Public Domain Notice with license fallback", + "NIST-PD-TNT": "NIST Public Domain Notice TNT variant", + "NIST-Software": "NIST Software License", + "NLOD-1.0": "Norwegian Licence for Open Government Data (NLOD) 1.0", + "NLOD-2.0": "Norwegian Licence for Open Government Data (NLOD) 2.0", + "NLPL": "No Limit Public License", + "Nokia": "Nokia Open Source License", + "NOSL": "Netizen Open Source License", + "Noweb": "Noweb License", + "NPL-1.0": "Netscape Public License v1.0", + "NPL-1.1": "Netscape Public License v1.1", + "NPOSL-3.0": "Non-Profit Open Software License 3.0", + "NRL": "NRL License", + "NTIA-PD": "NTIA Public Domain Notice", + "NTP": "NTP License", + "NTP-0": "NTP No Attribution", + "Nunit": "Nunit License", + "O-UDA-1.0": "Open Use of Data Agreement v1.0", + "OAR": "OAR License", + "OCCT-PL": "Open CASCADE Technology Public License", + "OCLC-2.0": "OCLC Research Public License 2.0", + "ODbL-1.0": "Open Data Commons Open Database License v1.0", + "ODC-By-1.0": "Open Data Commons Attribution License v1.0", + "OFFIS": "OFFIS License", + "OFL-1.0": "SIL Open Font License 1.0", + "OFL-1.0-no-RFN": "SIL Open Font License 1.0 with no Reserved Font Name", + "OFL-1.0-RFN": "SIL Open Font License 1.0 with Reserved Font Name", + "OFL-1.1": "SIL Open Font License 1.1", + "OFL-1.1-no-RFN": "SIL Open Font License 1.1 with no Reserved Font Name", + "OFL-1.1-RFN": "SIL Open Font License 1.1 with Reserved Font Name", + "OGC-1.0": "OGC Software License, Version 1.0", + "OGDL-Taiwan-1.0": "Taiwan Open Government Data License, version 1.0", + "OGL-Canada-2.0": "Open Government Licence - Canada", + "OGL-UK-1.0": "Open Government Licence v1.0", + "OGL-UK-2.0": "Open Government Licence v2.0", + "OGL-UK-3.0": "Open Government Licence v3.0", + "OGTSL": "Open Group Test Suite License", + "OLDAP-1.1": "Open LDAP Public License v1.1", + "OLDAP-1.2": "Open LDAP Public License v1.2", + "OLDAP-1.3": "Open LDAP Public License v1.3", + "OLDAP-1.4": "Open LDAP Public License v1.4", + "OLDAP-2.0": "Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)", + "OLDAP-2.0.1": "Open LDAP Public License v2.0.1", + "OLDAP-2.1": "Open LDAP Public License v2.1", + "OLDAP-2.2": "Open LDAP Public License v2.2", + "OLDAP-2.2.1": "Open LDAP Public License v2.2.1", + "OLDAP-2.2.2": "Open LDAP Public License 2.2.2", + "OLDAP-2.3": "Open LDAP Public License v2.3", + "OLDAP-2.4": "Open LDAP Public License v2.4", + "OLDAP-2.5": "Open LDAP Public License v2.5", + "OLDAP-2.6": "Open LDAP Public License v2.6", + "OLDAP-2.7": "Open LDAP Public License v2.7", + "OLDAP-2.8": "Open LDAP Public License v2.8", + "OLFL-1.3": "Open Logistics Foundation License Version 1.3", + "OML": "Open Market License", + "OpenMDW-1.0": "OpenMDW License Agreement v1.0", + "OpenPBS-2.3": "OpenPBS v2.3 Software License", + "OpenSSL": "OpenSSL License", + "OpenSSL-standalone": "OpenSSL License - standalone", + "OpenVision": "OpenVision License", + "OPL-1.0": "Open Public License v1.0", + "OPL-UK-3.0": "United Kingdom Open Parliament Licence v3.0", + "OPUBL-1.0": "Open Publication License v1.0", + "OSC-1.0": "OSC License 1.0", + "OSET-PL-2.1": "OSET Public License version 2.1", + "OSL-1.0": "Open Software License 1.0", + "OSL-1.1": "Open Software License 1.1", + "OSL-2.0": "Open Software License 2.0", + "OSL-2.1": "Open Software License 2.1", + "OSL-3.0": "Open Software License 3.0", + "OSSP": "OSSP License", + "PADL": "PADL License", + "ParaType-Free-Font-1.3": "ParaType Free Font Licensing Agreement v1.3", + "Parity-6.0.0": "The Parity Public License 6.0.0", + "Parity-7.0.0": "The Parity Public License 7.0.0", + "PDDL-1.0": "Open Data Commons Public Domain Dedication & License 1.0", + "PHP-3.0": "PHP License v3.0", + "PHP-3.01": "PHP License v3.01", + "Pixar": "Pixar License", + "pkgconf": "pkgconf License", + "Plexus": "Plexus Classworlds License", + "pnmstitch": "pnmstitch License", + "PolyForm-Noncommercial-1.0.0": "PolyForm Noncommercial License 1.0.0", + "PolyForm-Small-Business-1.0.0": "PolyForm Small Business License 1.0.0", + "PostgreSQL": "PostgreSQL License", + "PPL": "Peer Production License", + "PSF-2.0": "Python Software Foundation License 2.0", + "psfrag": "psfrag License", + "psutils": "psutils License", + "Python-2.0": "Python License 2.0", + "Python-2.0.1": "Python License 2.0.1", + "python-ldap": "Python ldap License", + "Qhull": "Qhull License", + "QPL-1.0": "Q Public License 1.0", + "QPL-1.0-INRIA-2004": "Q Public License 1.0 - INRIA 2004 variant", + "radvd": "radvd License", + "Rdisc": "Rdisc License", + "RHeCos-1.1": "Red Hat eCos Public License v1.1", + "RPL-1.1": "Reciprocal Public License 1.1", + "RPL-1.5": "Reciprocal Public License 1.5", + "RPSL-1.0": "RealNetworks Public Source License v1.0", + "RSA-MD": "RSA Message-Digest License", + "RSCPL": "Ricoh Source Code Public License", + "Ruby": "Ruby License", + "Ruby-pty": "Ruby pty extension license", + "SAX-PD": "Sax Public Domain Notice", + "SAX-PD-2.0": "Sax Public Domain Notice 2.0", + "Saxpath": "Saxpath License", + "SCEA": "SCEA Shared Source License", + "SchemeReport": "Scheme Language Report License", + "Sendmail": "Sendmail License", + "Sendmail-8.23": "Sendmail License 8.23", + "Sendmail-Open-Source-1.1": "Sendmail Open Source License v1.1", + "SGI-B-1.0": "SGI Free Software License B v1.0", + "SGI-B-1.1": "SGI Free Software License B v1.1", + "SGI-B-2.0": "SGI Free Software License B v2.0", + "SGI-OpenGL": "SGI OpenGL License", + "SGMLUG-PM": "SGMLUG Parser Materials License", + "SGP4": "SGP4 Permission Notice", + "SHL-0.5": "Solderpad Hardware License v0.5", + "SHL-0.51": "Solderpad Hardware License, Version 0.51", + "SimPL-2.0": "Simple Public License 2.0", + "SISSL": "Sun Industry Standards Source License v1.1", + "SISSL-1.2": "Sun Industry Standards Source License v1.2", + "SL": "SL License", + "Sleepycat": "Sleepycat License", + "SMAIL-GPL": "SMAIL General Public License", + "SMLNJ": "Standard ML of New Jersey License", + "SMPPL": "Secure Messaging Protocol Public License", + "SNIA": "SNIA Public License 1.1", + "snprintf": "snprintf License", + "SOFA": "SOFA Software License", + "softSurfer": "softSurfer License", + "Soundex": "Soundex License", + "Spencer-86": "Spencer License 86", + "Spencer-94": "Spencer License 94", + "Spencer-99": "Spencer License 99", + "SPL-1.0": "Sun Public License v1.0", + "ssh-keyscan": "ssh-keyscan License", + "SSH-OpenSSH": "SSH OpenSSH license", + "SSH-short": "SSH short notice", + "SSLeay-standalone": "SSLeay License - standalone", + "SSPL-1.0": "Server Side Public License, v 1", + "StandardML-NJ": "Standard ML of New Jersey License", + "SugarCRM-1.1.3": "SugarCRM Public License v1.1.3", + "SUL-1.0": "Sustainable Use License v1.0", + "Sun-PPP": "Sun PPP License", + "Sun-PPP-2000": "Sun PPP License (2000)", + "SunPro": "SunPro License", + "SWL": "Scheme Widget Library (SWL) Software License Agreement", + "swrule": "swrule License", + "Symlinks": "Symlinks License", + "TAPR-OHL-1.0": "TAPR Open Hardware License v1.0", + "TCL": "TCL\/TK License", + "TCP-wrappers": "TCP Wrappers License", + "TekHVC": "TekHVC License", + "TermReadKey": "TermReadKey License", + "TGPPL-1.0": "Transitive Grace Period Public Licence 1.0", + "ThirdEye": "ThirdEye License", + "threeparttable": "threeparttable License", + "TMate": "TMate Open Source License", + "TORQUE-1.1": "TORQUE v2.5+ Software License v1.1", + "TOSL": "Trusster Open Source License", + "TPDL": "Time::ParseDate License", + "TPL-1.0": "THOR Public License 1.0", + "TrustedQSL": "TrustedQSL License", + "TTWL": "Text-Tabs+Wrap License", + "TTYP0": "TTYP0 License", + "TU-Berlin-1.0": "Technische Universitaet Berlin License 1.0", + "TU-Berlin-2.0": "Technische Universitaet Berlin License 2.0", + "Ubuntu-font-1.0": "Ubuntu Font Licence v1.0", + "UCAR": "UCAR License", + "UCL-1.0": "Upstream Compatibility License v1.0", + "ulem": "ulem License", + "UMich-Merit": "Michigan\/Merit Networks License", + "Unicode-3.0": "Unicode License v3", + "Unicode-DFS-2015": "Unicode License Agreement - Data Files and Software (2015)", + "Unicode-DFS-2016": "Unicode License Agreement - Data Files and Software (2016)", + "Unicode-TOU": "Unicode Terms of Use", + "UnixCrypt": "UnixCrypt License", + "Unlicense": "The Unlicense", + "Unlicense-libtelnet": "Unlicense - libtelnet variant", + "Unlicense-libwhirlpool": "Unlicense - libwhirlpool variant", + "UnRAR": "UnRAR License", + "UPL-1.0": "Universal Permissive License v1.0", + "URT-RLE": "Utah Raster Toolkit Run Length Encoded License", + "Vim": "Vim License", + "Vixie-Cron": "Vixie Cron License", + "VOSTROM": "VOSTROM Public License for Open Source", + "VSL-1.0": "Vovida Software License v1.0", + "W3C": "W3C Software Notice and License (2002-12-31)", + "W3C-19980720": "W3C Software Notice and License (1998-07-20)", + "W3C-20150513": "W3C Software Notice and Document License (2015-05-13)", + "w3m": "w3m License", + "Watcom-1.0": "Sybase Open Watcom Public License 1.0", + "Widget-Workshop": "Widget Workshop License", + "WordNet": "WordNet License", + "Wsuipa": "Wsuipa License", + "WTFNMFPL": "Do What The F*ck You Want To But It's Not My Fault Public License", + "WTFPL": "Do What The F*ck You Want To Public License", + "wwl": "WWL License", + "wxWindows": "wxWindows Library License", + "X11": "X11 License", + "X11-distribute-modifications-variant": "X11 License Distribution Modification Variant", + "X11-no-permit-persons": "X11 no permit persons clause", + "X11-swapped": "X11 swapped final paragraphs", + "Xdebug-1.03": "Xdebug License v 1.03", + "Xerox": "Xerox License", + "Xfig": "Xfig License", + "XFree86-1.1": "XFree86 License 1.1", + "xinetd": "xinetd License", + "xkeyboard-config-Zinoviev": "xkeyboard-config Zinoviev License", + "xlock": "xlock License", + "Xnet": "X.Net License", + "xpp": "XPP License", + "XSkat": "XSkat License", + "xzoom": "xzoom License", + "YPL-1.0": "Yahoo! Public License v1.0", + "YPL-1.1": "Yahoo! Public License v1.1", + "Zed": "Zed License", + "Zeeff": "Zeeff License", + "Zend-2.0": "Zend License v2.0", + "Zimbra-1.3": "Zimbra Public License v1.3", + "Zimbra-1.4": "Zimbra Public License v1.4", + "Zlib": "zlib License", + "zlib-acknowledgement": "zlib\/libpng License with Acknowledgement", + "ZPL-1.1": "Zope Public License 1.1", + "ZPL-2.0": "Zope Public License 2.0", + "ZPL-2.1": "Zope Public License 2.1", + "389-exception": "389 Directory Server Exception", + "Asterisk-exception": "Asterisk exception", + "Asterisk-linking-protocols-exception": "Asterisk linking protocols exception", + "Autoconf-exception-2.0": "Autoconf exception 2.0", + "Autoconf-exception-3.0": "Autoconf exception 3.0", + "Autoconf-exception-generic": "Autoconf generic exception", + "Autoconf-exception-generic-3.0": "Autoconf generic exception for GPL-3.0", + "Autoconf-exception-macro": "Autoconf macro exception", + "Bison-exception-1.24": "Bison exception 1.24", + "Bison-exception-2.2": "Bison exception 2.2", + "Bootloader-exception": "Bootloader Distribution Exception", + "CGAL-linking-exception": "CGAL Linking Exception", + "Classpath-exception-2.0": "Classpath exception 2.0", + "Classpath-exception-2.0-short": "Classpath exception 2.0 - short", + "CLISP-exception-2.0": "CLISP exception 2.0", + "cryptsetup-OpenSSL-exception": "cryptsetup OpenSSL exception", + "Digia-Qt-LGPL-exception-1.1": "Digia Qt LGPL Exception version 1.1", + "DigiRule-FOSS-exception": "DigiRule FOSS License Exception", + "eCos-exception-2.0": "eCos exception 2.0", + "erlang-otp-linking-exception": "Erlang\/OTP Linking Exception", + "Fawkes-Runtime-exception": "Fawkes Runtime Exception", + "FLTK-exception": "FLTK exception", + "fmt-exception": "fmt exception", + "Font-exception-2.0": "Font exception 2.0", + "freertos-exception-2.0": "FreeRTOS Exception 2.0", + "GCC-exception-2.0": "GCC Runtime Library exception 2.0", + "GCC-exception-2.0-note": "GCC Runtime Library exception 2.0 - note variant", + "GCC-exception-3.1": "GCC Runtime Library exception 3.1", + "Gmsh-exception": "Gmsh exception", + "GNAT-exception": "GNAT exception", + "GNOME-examples-exception": "GNOME examples exception", + "GNU-compiler-exception": "GNU Compiler Exception", + "gnu-javamail-exception": "GNU JavaMail exception", + "GPL-3.0-389-ds-base-exception": "GPL-3.0 389 DS Base Exception", + "GPL-3.0-interface-exception": "GPL-3.0 Interface Exception", + "GPL-3.0-linking-exception": "GPL-3.0 Linking Exception", + "GPL-3.0-linking-source-exception": "GPL-3.0 Linking Exception (with Corresponding Source)", + "GPL-CC-1.0": "GPL Cooperation Commitment 1.0", + "GStreamer-exception-2005": "GStreamer Exception (2005)", + "GStreamer-exception-2008": "GStreamer Exception (2008)", + "harbour-exception": "harbour exception", + "i2p-gpl-java-exception": "i2p GPL+Java Exception", + "Independent-modules-exception": "Independent Module Linking exception", + "KiCad-libraries-exception": "KiCad Libraries Exception", + "kvirc-openssl-exception": "kvirc OpenSSL Exception", + "LGPL-3.0-linking-exception": "LGPL-3.0 Linking Exception", + "libpri-OpenH323-exception": "libpri OpenH323 exception", + "Libtool-exception": "Libtool Exception", + "Linux-syscall-note": "Linux Syscall Note", + "LLGPL": "LLGPL Preamble", + "LLVM-exception": "LLVM Exception", + "LZMA-exception": "LZMA exception", + "mif-exception": "Macros and Inline Functions Exception", + "mxml-exception": "mxml Exception", + "Nokia-Qt-exception-1.1": "Nokia Qt LGPL exception 1.1", + "OCaml-LGPL-linking-exception": "OCaml LGPL Linking Exception", + "OCCT-exception-1.0": "Open CASCADE Exception 1.0", + "OpenJDK-assembly-exception-1.0": "OpenJDK Assembly exception 1.0", + "openvpn-openssl-exception": "OpenVPN OpenSSL Exception", + "PCRE2-exception": "PCRE2 exception", + "polyparse-exception": "Polyparse Exception", + "PS-or-PDF-font-exception-20170817": "PS\/PDF font exception (2017-08-17)", + "QPL-1.0-INRIA-2004-exception": "INRIA QPL 1.0 2004 variant exception", + "Qt-GPL-exception-1.0": "Qt GPL exception 1.0", + "Qt-LGPL-exception-1.1": "Qt LGPL exception 1.1", + "Qwt-exception-1.0": "Qwt exception 1.0", + "romic-exception": "Romic Exception", + "RRDtool-FLOSS-exception-2.0": "RRDtool FLOSS exception 2.0", + "rsync-linking-exception": "rsync Linking Exception", + "SANE-exception": "SANE Exception", + "SHL-2.0": "Solderpad Hardware License v2.0", + "SHL-2.1": "Solderpad Hardware License v2.1", + "Simple-Library-Usage-exception": "Simple Library Usage Exception", + "sqlitestudio-OpenSSL-exception": "sqlitestudio OpenSSL exception", + "stunnel-exception": "stunnel Exception", + "SWI-exception": "SWI exception", + "Swift-exception": "Swift Exception", + "Texinfo-exception": "Texinfo exception", + "u-boot-exception-2.0": "U-Boot exception 2.0", + "UBDL-exception": "Unmodified Binary Distribution exception", + "Universal-FOSS-exception-1.0": "Universal FOSS Exception, Version 1.0", + "vsftpd-openssl-exception": "vsftpd OpenSSL exception", + "WxWindows-exception-3.1": "WxWindows Library Exception 3.1", + "x11vnc-openssl-exception": "x11vnc OpenSSL Exception" + } +} \ No newline at end of file diff --git a/schema/cyclonedx/spdx.xsd b/schema/cyclonedx/spdx.xsd index 41a27b02d..e94c265bd 100644 --- a/schema/cyclonedx/spdx.xsd +++ b/schema/cyclonedx/spdx.xsd @@ -2,7 +2,7 @@ + version="1.0-3.28.0"> @@ -57,6 +57,11 @@ Amazon Digital Services License + + + Advanced Cryptics Dictionary License + + Academic Free License v1.1 @@ -122,6 +127,11 @@ Aladdin Free Public License + + + ALGLIB Documentation License + + AMD newlib License @@ -327,6 +337,11 @@ Boehm-Demers-Weiser GC License (without fee) + + + Buena Onda License Agreement v1.1 + + Borceux license @@ -457,6 +472,11 @@ BSD 3-Clause Sun Microsystems + + + BSD 3-Clause Tso variant + + BSD 4-Clause "Original" or "Old" License @@ -497,6 +517,11 @@ BSD-Inferno-Nettverk + + + BSD Mark Modifications License + + BSD Protection License @@ -527,6 +552,11 @@ Boost Software License 1.0 + + + Buddy License + + Business Source License 1.1 @@ -567,6 +597,11 @@ Caldera License (without preamble) + + + Common Attack Pattern Enumeration and Classification License + + Catharon License @@ -1212,6 +1247,21 @@ Erlang Public License v1.1 + + + European Space Agency Public License – v2.4 – Permissive (Type 3) + + + + + European Space Agency Public License (ESA-PL) - V2.4 - Strong Copyleft (Type 1) + + + + + European Space Agency Public License – v2.4 – Weak Copyleft (Type 2) + + Etalab Open License 2.0 @@ -1737,6 +1787,11 @@ Historical Permission Notice and Disclaimer - sell variant + + + HPND - sell variant with safety critical systems clause + + HPND sell variant with MIT disclaimer @@ -1747,6 +1802,11 @@ HPND sell variant with MIT disclaimer - reverse + + + Historical Permission Notice and Disclaimer - SMC variant + + Historical Permission Notice and Disclaimer - University of California variant @@ -1762,6 +1822,11 @@ HTML Tidy License + + + hyphen-bulgarian License + + IBM PowerPC Initialization and Boot Software @@ -1852,6 +1917,11 @@ ISC Veillard variant + + + ISO permission notice + + Jam License @@ -2237,6 +2307,11 @@ MIT Open Group variant + + + MIT-STK License + + MIT testregex Variant @@ -2257,6 +2332,11 @@ MMIXware License + + + Minecraft Mod Public License v1.0.1 + + Motosoto License @@ -2422,6 +2502,11 @@ NIST Public Domain Notice with license fallback + + + NIST Public Domain Notice TNT variant + + NIST Software License @@ -2687,6 +2772,11 @@ Open Market License + + + OpenMDW License Agreement v1.0 + + OpenPBS v2.3 Software License @@ -2722,6 +2812,11 @@ Open Publication License v1.0 + + + OSC License 1.0 + + OSET Public License version 2.1 @@ -2752,11 +2847,21 @@ Open Software License 3.0 + + + OSSP License + + PADL License + + + ParaType Free Font Licensing Agreement v1.3 + + The Parity Public License 6.0.0 @@ -2977,6 +3082,11 @@ SGI OpenGL License + + + SGMLUG Parser Materials License + + SGP4 Permission Notice @@ -3162,6 +3272,11 @@ TCP Wrappers License + + + TekHVC License + + TermReadKey License @@ -3297,6 +3412,11 @@ Unlicense - libwhirlpool variant + + + UnRAR License + + Universal Permissive License v1.0 @@ -3312,6 +3432,11 @@ Vim License + + + Vixie Cron License + + VOSTROM Public License for Open Source @@ -3352,11 +3477,21 @@ Widget Workshop License + + + WordNet License + + Wsuipa License + + + Do What The F*ck You Want To But It's Not My Fault Public License + + Do What The F*ck You Want To Public License @@ -3382,6 +3517,11 @@ X11 License Distribution Modification Variant + + + X11 no permit persons clause + + X11 swapped final paragraphs @@ -3568,6 +3708,11 @@ Classpath exception 2.0 + + + Classpath exception 2.0 - short + + CLISP exception 2.0 @@ -3718,6 +3863,11 @@ KiCad Libraries Exception + + + kvirc OpenSSL Exception + + LGPL-3.0 Linking Exception @@ -3833,6 +3983,11 @@ RRDtool FLOSS exception 2.0 + + + rsync Linking Exception + + SANE Exception @@ -3848,6 +4003,16 @@ Solderpad Hardware License v2.1 + + + Simple Library Usage Exception + + + + + sqlitestudio OpenSSL exception + + stunnel Exception diff --git a/syft/format/cyclonedxjson/testdata/identify/1.7.json b/syft/format/cyclonedxjson/testdata/identify/1.7.json new file mode 100644 index 000000000..fc4b33720 --- /dev/null +++ b/syft/format/cyclonedxjson/testdata/identify/1.7.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "serialNumber": "urn:uuid:5208fea9-73dd-4624-b596-69fddccdb9e7", + "version": 1, + "metadata": { + "timestamp": "2023-09-29T12:02:02-04:00", + "tools": [ + { + "vendor": "anchore", + "name": "syft", + "version": "[not provided]" + } + ], + "component": { + "bom-ref": "a0ff99a6af10f11f", + "type": "file", + "name": "go.mod", + "version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd" + } + }, + "components": [ + { + "bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86", + "type": "library", + "name": "github.com/wagoodman/go-partybus", + "version": "v0.0.0-20230516145632-8ccac152c651", + "cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*", + "purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-module-file-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:metadataType", + "value": "GolangModMetadata" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "/go.mod" + } + ] + } + ] +} diff --git a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden index 7bdb196b9..f37de90e8 100644 --- a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,7 +1,7 @@ { - "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", "bomFormat": "CycloneDX", - "specVersion": "1.6", + "specVersion": "1.7", "serialNumber": "urn:uuid:redacted", "version": 1, "metadata": { diff --git a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxImageEncoder.golden b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxImageEncoder.golden index fab1acd75..075961803 100644 --- a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxImageEncoder.golden @@ -1,7 +1,7 @@ { - "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", "bomFormat": "CycloneDX", - "specVersion": "1.6", + "specVersion": "1.7", "serialNumber": "urn:uuid:redacted", "version": 1, "metadata": { diff --git a/syft/format/cyclonedxxml/testdata/identify/1.7.xml b/syft/format/cyclonedxxml/testdata/identify/1.7.xml new file mode 100644 index 000000000..2b62fee67 --- /dev/null +++ b/syft/format/cyclonedxxml/testdata/identify/1.7.xml @@ -0,0 +1,33 @@ + + + + 2023-09-29T11:48:10-04:00 + + + anchore + syft + [not provided] + + + + go.mod + sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd + + + + + github.com/wagoodman/go-partybus + v0.0.0-20230516145632-8ccac152c651 + cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:* + pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651 + + go-module-file-cataloger + go + GolangModMetadata + go-module + cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:* + /go.mod + + + + \ No newline at end of file diff --git a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden index 6d4345882..43b80fdeb 100644 --- a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,5 +1,5 @@ - + redacted diff --git a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxImageEncoder.golden b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxImageEncoder.golden index 7680d294d..58a21fc10 100644 --- a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxImageEncoder.golden @@ -1,5 +1,5 @@ - + redacted diff --git a/syft/format/internal/cyclonedxutil/encoder.go b/syft/format/internal/cyclonedxutil/encoder.go index 26da2de00..dc843734c 100644 --- a/syft/format/internal/cyclonedxutil/encoder.go +++ b/syft/format/internal/cyclonedxutil/encoder.go @@ -9,8 +9,6 @@ import ( "github.com/anchore/syft/syft/sbom" ) -const DefaultVersion = "1.6" - type Encoder struct { version cyclonedx.SpecVersion format cyclonedx.BOMFileFormat diff --git a/syft/format/internal/cyclonedxutil/versions.go b/syft/format/internal/cyclonedxutil/versions.go index 7c6e74bab..990eb6987 100644 --- a/syft/format/internal/cyclonedxutil/versions.go +++ b/syft/format/internal/cyclonedxutil/versions.go @@ -2,6 +2,10 @@ package cyclonedxutil import ( "fmt" + "maps" + "slices" + "strconv" + "strings" "github.com/CycloneDX/cyclonedx-go" @@ -13,59 +17,81 @@ const ( JSONFormatID sbom.FormatID = "cyclonedx-json" ) +const DefaultVersion = "1.7" + +var commonVersions = map[string]cyclonedx.SpecVersion{ + "1.2": cyclonedx.SpecVersion1_2, + "1.3": cyclonedx.SpecVersion1_3, + "1.4": cyclonedx.SpecVersion1_4, + "1.5": cyclonedx.SpecVersion1_5, + "1.6": cyclonedx.SpecVersion1_6, + DefaultVersion: cyclonedx.SpecVersion1_7, +} + +var allVersions = func() map[string]cyclonedx.SpecVersion { + out := map[string]cyclonedx.SpecVersion{ + "1.0": cyclonedx.SpecVersion1_0, + "1.1": cyclonedx.SpecVersion1_1, + } + maps.Copy(out, commonVersions) + return out +}() + func SupportedVersions(id sbom.FormatID) []string { - versions := []string{ - "1.2", - "1.3", - "1.4", - "1.5", - "1.6", + versionSet := commonVersions + if id == XMLFormatID { + versionSet = allVersions } - - if id != JSONFormatID { - // JSON format not supported for version < 1.2 - versions = append([]string{"1.0", "1.1"}, versions...) + versions := make([]string, 0, len(versionSet)) + for _, v := range versionSet { + versions = append(versions, v.String()) } - + slices.SortFunc(versions, versionSort) return versions } func SpecVersionFromString(v string) (cyclonedx.SpecVersion, error) { - switch v { - case "1.0": - return cyclonedx.SpecVersion1_0, nil - case "1.1": - return cyclonedx.SpecVersion1_1, nil - case "1.2": - return cyclonedx.SpecVersion1_2, nil - case "1.3": - return cyclonedx.SpecVersion1_3, nil - case "1.4": - return cyclonedx.SpecVersion1_4, nil - case "1.5": - return cyclonedx.SpecVersion1_5, nil - case "1.6": - return cyclonedx.SpecVersion1_6, nil + if specVersion, ok := allVersions[v]; ok { + return specVersion, nil } return -1, fmt.Errorf("unsupported CycloneDX version %q", v) } func VersionFromSpecVersion(spec cyclonedx.SpecVersion) string { - switch spec { - case cyclonedx.SpecVersion1_0: - return "1.0" - case cyclonedx.SpecVersion1_1: - return "1.1" - case cyclonedx.SpecVersion1_2: - return "1.2" - case cyclonedx.SpecVersion1_3: - return "1.3" - case cyclonedx.SpecVersion1_4: - return "1.4" - case cyclonedx.SpecVersion1_5: - return "1.5" - case cyclonedx.SpecVersion1_6: - return "1.6" + for version, specVersion := range allVersions { + if specVersion == spec { + return version + } } return "" } + +func versionSort(a string, b string) int { + partsA := strings.Split(a, ".") + partsB := strings.Split(b, ".") + lenA := len(partsA) + lenB := len(partsB) + for i := range max(lenA, lenB) { + if i >= lenA { + return -1 // 1 < 1.x + } + if i >= lenB { + return 1 // 1.x > 1 + } + partA, errA := strconv.ParseInt(partsA[i], 10, 64) + partB, errB := strconv.ParseInt(partsB[i], 10, 64) + if errA != nil || errB != nil { + // string compare if we can't parse one of the sides + strcmp := strings.Compare(partsA[i], partsB[i]) + if strcmp == 0 { + continue + } + return strcmp // not equal + } + if partA == partB { + continue + } + return int(partA - partB) + } + return 0 +} diff --git a/syft/format/internal/cyclonedxutil/versions_test.go b/syft/format/internal/cyclonedxutil/versions_test.go new file mode 100644 index 000000000..43582f121 --- /dev/null +++ b/syft/format/internal/cyclonedxutil/versions_test.go @@ -0,0 +1,107 @@ +package cyclonedxutil + +import ( + "go/constant" + "go/types" + "regexp" + "slices" + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/tools/go/packages" +) + +// Test_allSpecVersionsMapped loads the cyclonedx-go package's type information and enumerates every +// SpecVersion* constant it declares, asserting each one is present in our version maps. Upgrading +// cyclonedx-go to a release that adds a new spec version will fail this test until versions.go is updated. +func Test_allSpecVersionsMapped(t *testing.T) { + const pkgPath = "github.com/CycloneDX/cyclonedx-go" + + pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName | packages.NeedTypes}, pkgPath) + require.NoError(t, err) + require.Len(t, pkgs, 1) + + pkg := pkgs[0] + require.Empty(t, pkg.Errors, "errors loading %s", pkgPath) + require.NotNil(t, pkg.Types, "no type info loaded for %s", pkgPath) + + // allVersions is the superset (json/common versions merged in), so it covers every spec version we map. + mapped := make(map[cyclonedx.SpecVersion]struct{}) + for syftVersion, sv := range allVersions { + // make sure the key matches the version it's mapped to + require.Contains(t, sv.String(), syftVersion, "version %q is not correctly mapped to cyclonedx SpecVersion %q", syftVersion, sv.String()) + mapped[sv] = struct{}{} + } + + specVersionConst := regexp.MustCompile(`^SpecVersion\d`) + scope := pkg.Types.Scope() + found := 0 + for _, name := range scope.Names() { + if !specVersionConst.MatchString(name) { + continue + } + c, ok := scope.Lookup(name).(*types.Const) + if !ok { + continue + } + found++ + val, ok := constant.Int64Val(c.Val()) + require.Truef(t, ok, "could not read int value of cyclonedx.%s", name) + _, ok = mapped[cyclonedx.SpecVersion(val)] + + assert.Truef(t, ok, "cyclonedx.%s is not mapped in versions.go; add it when upgrading cyclonedx-go", name) + } + require.NotZero(t, found, "no SpecVersion* constants found in %s", pkgPath) +} + +func Test_versionSort(t *testing.T) { + // versionSort returns <0 when a0 when a>b; assert on sign rather than exact value. + sign := func(n int) int { + switch { + case n < 0: + return -1 + case n > 0: + return 1 + default: + return 0 + } + } + + tests := []struct { + name string + a string + b string + want int + }{ + {name: "equal single part", a: "1", b: "1", want: 0}, + {name: "equal two parts", a: "1.4", b: "1.4", want: 0}, + {name: "minor less than", a: "1.3", b: "1.4", want: -1}, + {name: "minor greater than", a: "1.6", b: "1.5", want: 1}, + {name: "major less than", a: "1.7", b: "2.0", want: -1}, + {name: "major greater than", a: "2.0", b: "1.7", want: 1}, + {name: "fewer parts sorts first", a: "1", b: "1.0", want: -1}, + {name: "more parts sorts last", a: "1.0", b: "1", want: 1}, + {name: "numeric not lexical (10 > 9)", a: "1.10", b: "1.9", want: 1}, + {name: "multi-digit major", a: "10.0", b: "9.0", want: 1}, + {name: "three parts equal", a: "1.4.0", b: "1.4.0", want: 0}, + {name: "three parts patch differs", a: "1.4.1", b: "1.4.0", want: 1}, + {name: "non-numeric equal falls through", a: "1.x", b: "1.x", want: 0}, + {name: "non-numeric string compare", a: "1.a", b: "1.b", want: -1}, + {name: "non-numeric side string compare", a: "1.2", b: "1.x", want: -1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, sign(versionSort(tt.a, tt.b))) + // comparator must be antisymmetric: swapping operands negates the sign. + assert.Equal(t, -tt.want, sign(versionSort(tt.b, tt.a))) + }) + } +} + +func Test_versionSort_sortsSlice(t *testing.T) { + versions := []string{"1.10", "1.2", "1", "2.0", "1.9", "1.0"} + slices.SortFunc(versions, versionSort) + assert.Equal(t, []string{"1", "1.0", "1.2", "1.9", "1.10", "2.0"}, versions) +} From 6a27678036527de43d0164cfdfa29e383a414306 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:29:23 +0000 Subject: [PATCH 002/168] chore(deps): bump the actions-minor-patch group across 2 directories with 6 updates (#4975) Bumps the actions-minor-patch group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [anchore/workflows/.github/workflows/codeql.yaml](https://github.com/anchore/workflows) | `0.7.0` | `0.7.2` | | [anchore/workflows/.github/workflows/check-version-available.yaml](https://github.com/anchore/workflows) | `0.7.0` | `0.7.2` | | [anchore/workflows/.github/workflows/check-gate.yaml](https://github.com/anchore/workflows) | `0.7.0` | `0.7.2` | | [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `6.0.3` | | [anchore/workflows/.github/workflows/release-install-script.yaml](https://github.com/anchore/workflows) | `0.7.0` | `0.7.2` | Bumps the actions-minor-patch group with 1 update in the /.github/actions/bootstrap directory: [anchore/go-make](https://github.com/anchore/go-make). Updates `anchore/workflows/.github/workflows/codeql.yaml` from 0.7.0 to 0.7.2 - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5...b0c30a80409130d329aaa356fd64a34d8c0b3375) Updates `anchore/workflows/.github/workflows/check-version-available.yaml` from 0.7.0 to 0.7.2 - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5...b0c30a80409130d329aaa356fd64a34d8c0b3375) Updates `anchore/workflows/.github/workflows/check-gate.yaml` from 0.7.0 to 0.7.2 - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5...b0c30a80409130d329aaa356fd64a34d8c0b3375) Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) Updates `anchore/workflows/.github/workflows/release-install-script.yaml` from 0.7.0 to 0.7.2 - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5...b0c30a80409130d329aaa356fd64a34d8c0b3375) Updates `anchore/go-make` from 0.5.0 to 0.6.0 - [Release notes](https://github.com/anchore/go-make/releases) - [Commits](https://github.com/anchore/go-make/compare/9de27be11ed73e2f9d5406a836a492b7d8aa1225...39fe5f71112d4dceb3ff0a92a40f272f067fc457) --- updated-dependencies: - dependency-name: anchore/workflows/.github/workflows/codeql.yaml dependency-version: 0.7.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: anchore/workflows/.github/workflows/check-version-available.yaml dependency-version: 0.7.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: anchore/workflows/.github/workflows/check-gate.yaml dependency-version: 0.7.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: anchore/workflows/.github/workflows/release-install-script.yaml dependency-version: 0.7.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: anchore/go-make dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/bootstrap/action.yaml | 2 +- .github/workflows/codeql.yaml | 2 +- .github/workflows/release.yaml | 8 ++++---- .github/workflows/validate-github-actions.yaml | 2 +- .github/workflows/validations.yaml | 14 +++++++------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 468ed95e1..26067046f 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -30,7 +30,7 @@ runs: using: "composite" steps: - name: Setup go + go-make tooling - uses: anchore/go-make/.github/actions/setup@9de27be11ed73e2f9d5406a836a492b7d8aa1225 # v0.5.0 + uses: anchore/go-make/.github/actions/setup@39fe5f71112d4dceb3ff0a92a40f272f067fc457 # v0.6.0 with: go-version: ${{ inputs.go-version }} cache-key-prefix: ${{ inputs.cache-key-prefix }} diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 7a0a5654f..6384d5959 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -13,7 +13,7 @@ permissions: {} jobs: analyze: name: Analyze - uses: anchore/workflows/.github/workflows/codeql.yaml@b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5 # v0.7.0 + uses: anchore/workflows/.github/workflows/codeql.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 permissions: security-events: write packages: read diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d93313ed3..f3387ce97 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,7 +27,7 @@ jobs: if: ${{ github.event.inputs.phase == 'all' }} permissions: contents: read # required for fetching tags - uses: anchore/workflows/.github/workflows/check-version-available.yaml@b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5 # v0.7.0 + uses: anchore/workflows/.github/workflows/check-version-available.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 with: version: ${{ github.event.inputs.version }} @@ -36,7 +36,7 @@ jobs: permissions: contents: read checks: read # required for getting the status of specific check names - uses: anchore/workflows/.github/workflows/check-gate.yaml@b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5 # v0.7.0 + uses: anchore/workflows/.github/workflows/check-gate.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 with: # these are checks that should be run on pull-request and merges to main. # we do NOT want to kick off a release if these have not been verified on main. @@ -58,7 +58,7 @@ jobs: packages: write # required for publishing release artifacts to GitHub packages id-token: write # required for keyless signing (cosign/sigstore OIDC) steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: fetch-depth: 0 persist-credentials: true @@ -107,7 +107,7 @@ jobs: if: ${{ always() && (needs.release.result == 'success' || github.event.inputs.phase == 'install-script-only') }} permissions: contents: read # required for the reusable workflow to check out the repo and publish the install script - uses: anchore/workflows/.github/workflows/release-install-script.yaml@b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5 # v0.7.0 + uses: anchore/workflows/.github/workflows/release-install-script.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 with: tag: ${{ github.event.inputs.version }} secrets: diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index 9810f60f5..d4c38eb98 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -20,7 +20,7 @@ jobs: contents: read security-events: write # for uploading SARIF results steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index f44181164..e7eea1288 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -28,7 +28,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false @@ -50,7 +50,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false @@ -72,7 +72,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false @@ -99,7 +99,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false @@ -129,7 +129,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false @@ -173,7 +173,7 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false @@ -207,7 +207,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: persist-credentials: false From 00ca43d24aef65f256dbadcc9f00f3b5f9360181 Mon Sep 17 00:00:00 2001 From: Kursat Topcuoglu Date: Mon, 15 Jun 2026 18:34:02 +0300 Subject: [PATCH 003/168] fix: catalog uv PEP 723 script lockfiles (*.py.lock) (#4950) Signed-off-by: Kursat Topcuoglu <7313835+ktopcuoglu@users.noreply.github.com> Co-authored-by: Kursat Topcuoglu <7313835+ktopcuoglu@users.noreply.github.com> --- syft/pkg/cataloger/python/capabilities.yaml | 1 + syft/pkg/cataloger/python/cataloger.go | 5 ++++- syft/pkg/cataloger/python/cataloger_test.go | 1 + .../cataloger/python/testdata/glob-paths/src/script.py.lock | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 syft/pkg/cataloger/python/testdata/glob-paths/src/script.py.lock diff --git a/syft/pkg/cataloger/python/capabilities.yaml b/syft/pkg/cataloger/python/capabilities.yaml index 30211eb71..dfbadc272 100644 --- a/syft/pkg/cataloger/python/capabilities.yaml +++ b/syft/pkg/cataloger/python/capabilities.yaml @@ -113,6 +113,7 @@ catalogers: method: glob # AUTO-GENERATED criteria: # AUTO-GENERATED - '**/uv.lock' + - '**/*.py.lock' metadata_types: # AUTO-GENERATED - pkg.PythonUvLockEntry package_types: # AUTO-GENERATED diff --git a/syft/pkg/cataloger/python/cataloger.go b/syft/pkg/cataloger/python/cataloger.go index dffc7b3fb..d7854b784 100644 --- a/syft/pkg/cataloger/python/cataloger.go +++ b/syft/pkg/cataloger/python/cataloger.go @@ -23,7 +23,10 @@ func NewPackageCataloger(cfg CatalogerConfig) pkg.Cataloger { WithParserByGlobs(poetryLockParser.parsePoetryLock, "**/poetry.lock"). WithParserByGlobs(pipfileLockParser.parsePipfileLock, "**/Pipfile.lock"). WithParserByGlobs(setupFileParser.parseSetupFile, "**/setup.py"). - WithParserByGlobs(uvLockParser.parseUvLock, "**/uv.lock"). + // uv lock files are named "uv.lock", but PEP 723 script lock files + // (created by "uv lock --script .py") are named ".py.lock" + // and use the same format, so catalog both. + WithParserByGlobs(uvLockParser.parseUvLock, "**/uv.lock", "**/*.py.lock"). WithParserByGlobs(pdmLockParser.parsePdmLock, "**/pdm.lock") } diff --git a/syft/pkg/cataloger/python/cataloger_test.go b/syft/pkg/cataloger/python/cataloger_test.go index 3d9f64311..8da2c6092 100644 --- a/syft/pkg/cataloger/python/cataloger_test.go +++ b/syft/pkg/cataloger/python/cataloger_test.go @@ -501,6 +501,7 @@ func Test_IndexCataloger_Globs(t *testing.T) { "src/poetry.lock", "src/Pipfile.lock", "src/uv.lock", + "src/script.py.lock", "src/pdm.lock", }, }, diff --git a/syft/pkg/cataloger/python/testdata/glob-paths/src/script.py.lock b/syft/pkg/cataloger/python/testdata/glob-paths/src/script.py.lock new file mode 100644 index 000000000..5ffba7b57 --- /dev/null +++ b/syft/pkg/cataloger/python/testdata/glob-paths/src/script.py.lock @@ -0,0 +1 @@ +bogus From cff5a05681af9ea70d6c64006842a70ceb953baa Mon Sep 17 00:00:00 2001 From: David Dashti <47575784+Dashtid@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:15:32 +0200 Subject: [PATCH 004/168] fix(dpkg): extract License field for opkg/ipkg entries (#4963) * fix(dpkg): extract License field for opkg/ipkg entries opkg and ipkg use the dpkg cataloger but declare the package License inline in the status DB (unlike Debian dpkg, where licenses live in copyright files). The cataloger silently dropped the License field at mapstructure decode time, so all opkg-managed packages reported empty licenses. This adds the field to the intermediate decode struct and the public DpkgDBEntry, and populates licenses in newDpkgPackage using the alpine cataloger's pattern: try license.ParseExpression first to keep valid SPDX expressions whole, fall back to whitespace splitting for space-separated lists. Standard Debian dpkg status files never carry a License field per Debian policy, so the new path is a no-op for them; the existing copyright-file lookup in addLicenses is unaffected. Closes #4940 Signed-off-by: David Dashti <47575784+Dashtid@users.noreply.github.com> * remove license from dpkg metadata struct Signed-off-by: Alex Goodman * restore format snapshot files Signed-off-by: Alex Goodman * add additional tests Signed-off-by: Alex Goodman --------- Signed-off-by: David Dashti <47575784+Dashtid@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/debian/package.go | 35 +++++-- syft/pkg/cataloger/debian/package_test.go | 98 +++++++++++++++++++ .../pkg/cataloger/debian/parse_deb_archive.go | 2 +- syft/pkg/cataloger/debian/parse_dpkg_db.go | 71 ++++++++------ .../cataloger/debian/parse_dpkg_db_test.go | 54 +++++++++- .../debian/testdata/var/lib/opkg/status | 30 ++++++ 6 files changed, 246 insertions(+), 44 deletions(-) create mode 100644 syft/pkg/cataloger/debian/testdata/var/lib/opkg/status diff --git a/syft/pkg/cataloger/debian/package.go b/syft/pkg/cataloger/debian/package.go index 88102da6e..364963516 100644 --- a/syft/pkg/cataloger/debian/package.go +++ b/syft/pkg/cataloger/debian/package.go @@ -13,6 +13,7 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" ) @@ -23,21 +24,26 @@ const ( docsPath = "/usr/share/doc" ) -func newDpkgPackage(ctx context.Context, d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package { - // TODO: separate pr to license refactor, but explore extracting dpkg-specific license parsing into a separate function - var licenses []pkg.License +func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package { + // the License field is empty for standard Debian dpkg entries (licenses live in copyright files), + // but opkg/ipkg derivatives carry it inline in the status DB — extract it here so those packages + // report licenses without requiring per-package copyright lookups. The license is not persisted on + // the final entry, so convert the raw metadata into the entry just-in-time here. + licenses := extractDeclaredLicenses(ctx, d.License, dbLocation) + + entry := d.toDpkgEntry() locations := file.NewLocationSet(dbLocation) locations.Add(evidence...) p := pkg.Package{ - Name: d.Package, - Version: d.Version, + Name: entry.Package, + Version: entry.Version, Licenses: pkg.NewLicenseSet(licenses...), Locations: locations, - PURL: packageURL(d, release), + PURL: packageURL(entry, release), Type: pkg.DebPkg, - Metadata: d, + Metadata: entry, } if resolver != nil { @@ -55,6 +61,21 @@ func newDpkgPackage(ctx context.Context, d pkg.DpkgDBEntry, dbLocation file.Loca return p } +// extractDeclaredLicenses converts a License field from the status DB into a license set. Returns nil +// for empty input so standard dpkg entries (which never declare License inline) incur no allocation. +// Mirrors the alpine cataloger's approach: keep the value whole if it parses as a valid SPDX expression, +// otherwise split on whitespace to handle space-separated lists. +func extractDeclaredLicenses(ctx context.Context, raw string, dbLocation file.Location) []pkg.License { + if raw == "" { + return nil + } + licenseStrings := []string{raw} + if _, err := license.ParseExpression(raw); err != nil { + licenseStrings = strings.Fields(raw) + } + return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...) +} + func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package { p := pkg.Package{ Name: metadata.Package, diff --git a/syft/pkg/cataloger/debian/package_test.go b/syft/pkg/cataloger/debian/package_test.go index a52659739..227ebc9d5 100644 --- a/syft/pkg/cataloger/debian/package_test.go +++ b/syft/pkg/cataloger/debian/package_test.go @@ -1,10 +1,14 @@ package debian import ( + "context" + "sort" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" ) @@ -111,3 +115,97 @@ func Test_packageURL(t *testing.T) { }) } } + +func Test_extractDeclaredLicenses(t *testing.T) { + ctx := context.Background() + dbLocation := file.NewLocation("/var/lib/opkg/status") + + tests := []struct { + name string + raw string + expected []string + }{ + { + name: "empty input returns nil", + raw: "", + expected: nil, + }, + { + name: "single SPDX identifier kept whole", + raw: "MIT", + expected: []string{"MIT"}, + }, + { + name: "valid SPDX expression kept whole", + raw: "Apache-2.0 OR MIT", + expected: []string{"Apache-2.0 OR MIT"}, + }, + { + name: "non-expression space-separated list is split", + raw: "GPL-2.0 BSD-3-Clause", + expected: []string{"GPL-2.0", "BSD-3-Clause"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := extractDeclaredLicenses(ctx, test.raw, dbLocation) + var gotValues []string + for _, l := range got { + gotValues = append(gotValues, l.Value) + } + // NewLicensesFromLocationWithContext does not guarantee output order + sort.Strings(gotValues) + want := append([]string(nil), test.expected...) + sort.Strings(want) + if diff := cmp.Diff(want, gotValues); diff != "" { + t.Errorf("unexpected licenses (-want +got):\n%s", diff) + } + }) + } +} + +func Test_newDpkgPackage_declaredLicense(t *testing.T) { + // the License field is not persisted on pkg.DpkgDBEntry, so this guards that the inline opkg/ipkg license + // declared on the raw metadata still flows into the built package's license set + tests := []struct { + name string + metadata dpkgExtractedMetadata + expected []string + }{ + { + name: "no declared license", + metadata: dpkgExtractedMetadata{Package: "apt", Version: "1.8.2"}, + expected: nil, + }, + { + name: "inline license flows to package", + metadata: dpkgExtractedMetadata{Package: "dropbear", Version: "2024.85-r0", License: "MIT"}, + expected: []string{"MIT"}, + }, + { + name: "space-separated licenses split into the set", + metadata: dpkgExtractedMetadata{Package: "busybox", Version: "1.36.1", License: "GPL-2.0 BSD-3-Clause"}, + expected: []string{"BSD-3-Clause", "GPL-2.0"}, + }, + { + name: "valid SPDX expression kept whole", + metadata: dpkgExtractedMetadata{Package: "curl", Version: "8.5.0", License: "Apache-2.0 OR MIT"}, + expected: []string{"Apache-2.0 OR MIT"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := newDpkgPackage(context.Background(), test.metadata, file.NewLocation("/var/lib/opkg/status"), nil, nil) + + var got []string + for _, l := range p.Licenses.ToSlice() { + got = append(got, l.Value) + } + // the license set does not guarantee output order + sort.Strings(got) + require.Equal(t, test.expected, got) + }) + } +} diff --git a/syft/pkg/cataloger/debian/parse_deb_archive.go b/syft/pkg/cataloger/debian/parse_deb_archive.go index 0f977c05e..b3b5f923f 100644 --- a/syft/pkg/cataloger/debian/parse_deb_archive.go +++ b/syft/pkg/cataloger/debian/parse_deb_archive.go @@ -131,7 +131,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { if len(entries) == 0 { return nil, fmt.Errorf("no package entries found in control file") } - entry := pkg.DpkgArchiveEntry(entries[0]) + entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry()) metadata = &entry case "md5sums": // parseDpkgMD5Info already streams via bufio.Scanner diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db.go b/syft/pkg/cataloger/debian/parse_dpkg_db.go index 1a2e72e50..be9f04448 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db.go @@ -42,7 +42,7 @@ func parseDpkgDB(ctx context.Context, resolver file.Resolver, env *generic.Envir dbLoc := reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation) var pkgs []pkg.Package - _ = sync.CollectSlice(&ctx, cataloging.ExecutorFile, sync.ToSeq(metadata), func(m pkg.DpkgDBEntry) (pkg.Package, error) { + _ = sync.CollectSlice(&ctx, cataloging.ExecutorFile, sync.ToSeq(metadata), func(m dpkgExtractedMetadata) (pkg.Package, error) { return newDpkgPackage(ctx, m, dbLoc, resolver, env.LinuxRelease, findDpkgInfoFiles(m.Package, resolver, reader.Location)...), nil }, &pkgs) @@ -77,10 +77,12 @@ func findDpkgInfoFiles(name string, resolver file.Resolver, dbLocation file.Loca return locations } -// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed. -func parseDpkgStatus(reader io.Reader) ([]pkg.DpkgDBEntry, error) { +// parseDpkgStatus is a parser function for Debian DB status contents, returning the raw metadata for all Debian +// packages listed. Conversion to pkg.DpkgDBEntry is deferred to the package-building stage so that fields which +// are not part of the final entry (e.g. License) remain available to the caller. +func parseDpkgStatus(reader io.Reader) ([]dpkgExtractedMetadata, error) { buffedReader := bufio.NewReader(reader) - var metadata []pkg.DpkgDBEntry + var metadata []dpkgExtractedMetadata continueProcessing := true for continueProcessing { @@ -117,10 +119,41 @@ type dpkgExtractedMetadata struct { Depends string `mapstructure:"Depends"` PreDepends string `mapstructure:"PreDepends"` // note: original doc is Pre-Depends Status string `mapstructure:"Status"` + License string `mapstructure:"License"` + Conffiles string `mapstructure:"Conffiles"` +} + +func (d *dpkgExtractedMetadata) toDpkgEntry() pkg.DpkgDBEntry { + entry := pkg.DpkgDBEntry{ + Package: d.Package, + Source: d.Source, + Version: d.Version, + SourceVersion: d.SourceVersion, + Architecture: d.Architecture, + Maintainer: d.Maintainer, + InstalledSize: d.InstalledSize, + Description: d.Description, + Provides: splitPkgList(d.Provides), + Depends: splitPkgList(d.Depends), + PreDepends: splitPkgList(d.PreDepends), + // note: licenses and conffiles are handled separately + } + + // there may be an optional conffiles section that we should persist as files + if d.Conffiles != "" { + entry.Files = parseDpkgConffileInfo(strings.NewReader(d.Conffiles)) + } + + if entry.Files == nil { + // ensure the default value for a collection is never nil since this may be shown as JSON + entry.Files = make([]pkg.DpkgFileRecord, 0) + } + + return entry } // parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader. -func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgDBEntry, error) { +func parseDpkgStatusEntry(reader *bufio.Reader) (*dpkgExtractedMetadata, error) { var retErr error dpkgFields, err := extractAllFields(reader) if err != nil { @@ -154,33 +187,7 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgDBEntry, error) { return nil, retErr } - entry := pkg.DpkgDBEntry{ - Package: raw.Package, - Source: raw.Source, - Version: raw.Version, - SourceVersion: raw.SourceVersion, - Architecture: raw.Architecture, - Maintainer: raw.Maintainer, - InstalledSize: raw.InstalledSize, - Description: raw.Description, - Provides: splitPkgList(raw.Provides), - Depends: splitPkgList(raw.Depends), - PreDepends: splitPkgList(raw.PreDepends), - } - - // there may be an optional conffiles section that we should persist as files - if conffilesSection, exists := dpkgFields["Conffiles"]; exists && conffilesSection != nil { - if sectionStr, ok := conffilesSection.(string); ok { - entry.Files = parseDpkgConffileInfo(strings.NewReader(sectionStr)) - } - } - - if entry.Files == nil { - // ensure the default value for a collection is never nil since this may be shown as JSON - entry.Files = make([]pkg.DpkgFileRecord, 0) - } - - return &entry, retErr + return &raw, retErr } func splitPkgList(pkgList string) (ret []string) { diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db_test.go b/syft/pkg/cataloger/debian/parse_dpkg_db_test.go index 1cca3d9f6..e423c0a08 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db_test.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db_test.go @@ -20,9 +20,12 @@ import ( func Test_parseDpkgStatus(t *testing.T) { tests := []struct { - name string - expected []pkg.DpkgDBEntry - fixturePath string + name string + expected []pkg.DpkgDBEntry + // expectedLicenses is the raw License value parsed for each entry (parallel to expected). License is not + // persisted on pkg.DpkgDBEntry, so it is asserted separately from the raw extracted metadata. + expectedLicenses []string + fixturePath string }{ { name: "single package", @@ -237,6 +240,37 @@ func Test_parseDpkgStatus(t *testing.T) { }, }, }, + { + name: "opkg status with license field", + fixturePath: "testdata/var/lib/opkg/status", + expected: []pkg.DpkgDBEntry{ + { + Package: "dropbear", + Version: "2024.85-r0", + Architecture: "x86_64", + Description: "Small SSH server and client.", + Depends: []string{"libc", "zlib"}, + Files: []pkg.DpkgFileRecord{}, + }, + { + Package: "busybox", + Version: "1.36.1-r3", + Architecture: "x86_64", + Description: "Single executable providing many common UNIX utilities.", + Depends: []string{"libc"}, + Files: []pkg.DpkgFileRecord{}, + }, + { + Package: "kernel-modules", + Version: "6.6.0-r0", + Architecture: "x86_64", + Description: "Loadable kernel modules with mixed licensing.", + Depends: []string{"kmod"}, + Files: []pkg.DpkgFileRecord{}, + }, + }, + expectedLicenses: []string{"MIT", "GPL-2.0-only", "GPL-2.0 BSD-3-Clause"}, + }, { name: "deinstall status packages are ignored", fixturePath: "testdata/var/lib/dpkg/status.d/deinstall", @@ -278,12 +312,24 @@ func Test_parseDpkgStatus(t *testing.T) { reader := bufio.NewReader(f) - entries, err := parseDpkgStatus(reader) + raw, err := parseDpkgStatus(reader) require.NoError(t, err) + // convert the raw metadata into the final entries just-in-time, mirroring the package-building stage + var entries []pkg.DpkgDBEntry + var licenses []string + for _, r := range raw { + entries = append(entries, r.toDpkgEntry()) + licenses = append(licenses, r.License) + } + if diff := cmp.Diff(test.expected, entries); diff != "" { t.Errorf("unexpected entry (-want +got):\n%s", diff) } + + if test.expectedLicenses != nil { + require.Equal(t, test.expectedLicenses, licenses) + } }) } } diff --git a/syft/pkg/cataloger/debian/testdata/var/lib/opkg/status b/syft/pkg/cataloger/debian/testdata/var/lib/opkg/status new file mode 100644 index 000000000..ed4b15038 --- /dev/null +++ b/syft/pkg/cataloger/debian/testdata/var/lib/opkg/status @@ -0,0 +1,30 @@ +Package: dropbear +Version: 2024.85-r0 +Depends: libc, zlib +Status: install user installed +Architecture: x86_64 +Installed-Time: 1714492800 +License: MIT +Section: net +Description: Small SSH server and client. + +Package: busybox +Version: 1.36.1-r3 +Depends: libc +Status: install user installed +Architecture: x86_64 +Installed-Time: 1714492800 +License: GPL-2.0-only +Section: base +Description: Single executable providing many common UNIX utilities. + +Package: kernel-modules +Version: 6.6.0-r0 +Depends: kmod +Status: install user installed +Architecture: x86_64 +Installed-Time: 1714492800 +License: GPL-2.0 BSD-3-Clause +Section: kernel +Description: Loadable kernel modules with mixed licensing. + From 8d48a8b8c2b93aced7c056f5e6d1965fc54cefa4 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 16 Jun 2026 10:41:33 -0400 Subject: [PATCH 005/168] ensure we have a snapshot build for cli tests (#4981) Signed-off-by: Alex Goodman --- .make/main.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.make/main.go b/.make/main.go index c67dc3424..274462506 100644 --- a/.make/main.go +++ b/.make/main.go @@ -56,11 +56,14 @@ func main() { }, }, - // cli tests: native go-make Task. Requires SYFT_BINARY_LOCATION pointing at - // an *absolute* path to the snapshot binary. Intentionally does NOT depend - // on snapshot: in CI we download a pre-built snapshot artifact and re-running - // goreleaser here would both burn ~10m and clobber the downloaded binary. - // Locally, the failure message tells you to run `make snapshot` first. + // cli tests: native go-make Task. Runs SYFT_BINARY_LOCATION at an *absolute* + // path to the snapshot binary. Builds the snapshot only when the binary is + // missing rather than depending on the snapshot task unconditionally: in + // validations.yaml CI we download a pre-built snapshot artifact, so the binary + // already exists and rebuilding would both burn ~10m and clobber it. When + // `make test` runs cold (e.g. the release pipeline) or locally with no + // snapshot, we build a single-target snapshot (current OS/arch only) since + // that's all the CLI tests need. Task{ Name: "cli", Description: "Run CLI tests", @@ -68,7 +71,8 @@ func main() { Run: func() { bin := snapshotBinPath() if !file.Exists(bin) { - panic(fmt.Sprintf("snapshot binary not found at %s; run `make snapshot` first", bin)) + Log("snapshot binary not found at %s; building single-target snapshot", bin) + Run("make snapshot:single-target") } Log("testing binary: %s", bin) Run( From 92ae4d44c5df09496e541cae51a11c3caef6ac4e Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Tue, 16 Jun 2026 12:02:42 -0400 Subject: [PATCH 006/168] fix: .net deps.json cataloger no longer shows phantom pkgs (#4971) Signed-off-by: Rez Moss --- .../cataloger/dotnet/deps_binary_cataloger.go | 8 ++ .../dotnet/deps_binary_cataloger_test.go | 97 +++++++++++++++++++ .../MyApp.deps.json | 62 ++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 syft/pkg/cataloger/dotnet/deps_binary_cataloger_test.go create mode 100644 syft/pkg/cataloger/dotnet/testdata/deps-with-referenceassembly/MyApp.deps.json diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index 44c0a9e97..105c98c47 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -292,6 +292,14 @@ func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) (* } lp := doc.PackagesByNameVersion[nameVersion] + if lp.Library != nil && + lp.Library.Type == "referenceassembly" && + lp.Library.Sha512 == "" && + lp.Library.Path == "" { + skippedDepPkgs[nameVersion] = lp + continue + } + if config.ExcludeProjectReferences && lp.Library != nil && lp.Library.Type == "project" { skippedDepPkgs[nameVersion] = lp continue diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger_test.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger_test.go new file mode 100644 index 000000000..9ac5bb517 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger_test.go @@ -0,0 +1,97 @@ +package dotnet + +import ( + "encoding/json" + "os" + "testing" + + "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +func Test_packagesFromLogicalDepsJSON_skipsReferenceAssemblies(t *testing.T) { + mkPkg := func(name, version, libType, sha, path string) logicalDepsJSONPackage { + nv := name + "/" + version + return logicalDepsJSONPackage{ + NameVersion: nv, + Library: &depsLibrary{Type: libType, Sha512: sha, Path: path}, + Targets: []depsTarget{ + {Runtime: map[string]map[string]string{"lib/" + name + ".dll": {}}}, + }, + RuntimePathsByRelativeDLLPath: map[string]string{name + ".dll": "lib/" + name + ".dll"}, + ResourcePathsByRelativeDLLPath: map[string]string{}, + CompilePathsByRelativeDLLPath: map[string]string{}, + NativePaths: strset.New(), + } + } + + realPkg := mkPkg("Real", "1.0.0", "package", "abc", "real/1.0.0") + phantom := mkPkg("Phantom", "2.0.0", "referenceassembly", "", "") + keptRef := mkPkg("KeptRef", "3.0.0", "referenceassembly", "xyz", "p") + + doc := logicalDepsJSON{ + Location: file.NewLocation("/app/Test.deps.json"), + PackagesByNameVersion: map[string]logicalDepsJSONPackage{ + realPkg.NameVersion: realPkg, + phantom.NameVersion: phantom, + keptRef.NameVersion: keptRef, + }, + PackageNameVersions: strset.New(realPkg.NameVersion, phantom.NameVersion, keptRef.NameVersion), + } + + cfg := DefaultCatalogerConfig(). + WithDepPackagesMustClaimDLL(false). + WithDepPackagesMustHaveDLL(false) + + _, pkgs, _ := packagesFromLogicalDepsJSON(doc, cfg) + + names := make(map[string]string) + for _, p := range pkgs { + names[p.Name] = p.Version + } + + assert.Equal(t, "1.0.0", names["Real"]) + assert.Equal(t, "3.0.0", names["KeptRef"], "ref-asm w/ sha kept") + assert.NotContains(t, names, "Phantom", "ref-asm w/o evidence skipped") + + for _, p := range pkgs { + assert.Equal(t, pkg.DotnetPkg, p.Type) + } +} + +func Test_packagesFromLogicalDepsJSON_fixture_referenceassembly(t *testing.T) { + const fixturePath = "testdata/deps-with-referenceassembly/MyApp.deps.json" + + raw, err := os.ReadFile(fixturePath) + require.NoError(t, err) + + var deps depsJSON + require.NoError(t, json.Unmarshal(raw, &deps)) + deps.Location = file.NewLocation("/app/MyApp.deps.json") + + doc := getLogicalDepsJSON(deps, nil) + doc.Location = deps.Location + + cfg := DefaultCatalogerConfig(). + WithDepPackagesMustClaimDLL(false). + WithDepPackagesMustHaveDLL(false) + + _, pkgs, _ := packagesFromLogicalDepsJSON(doc, cfg) + + names := make(map[string]string) + for _, p := range pkgs { + names[p.Name] = p.Version + } + + assert.Equal(t, "13.0.3", names["Newtonsoft.Json"]) + assert.NotContains(t, names, "Microsoft.AspNetCore.DataProtection") + assert.NotContains(t, names, "Microsoft.AspNetCore.DataProtection.Abstractions") + + for _, p := range pkgs { + assert.NotEqual(t, "10.0.0.0", p.Version, "no 10.0.0.0 phantom") + } +} diff --git a/syft/pkg/cataloger/dotnet/testdata/deps-with-referenceassembly/MyApp.deps.json b/syft/pkg/cataloger/dotnet/testdata/deps-with-referenceassembly/MyApp.deps.json new file mode 100644 index 000000000..240b2635d --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/deps-with-referenceassembly/MyApp.deps.json @@ -0,0 +1,62 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v10.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v10.0": { + "MyApp/1.0.0": { + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "Microsoft.AspNetCore.DataProtection": "10.0.0.0" + }, + "runtime": { + "MyApp.dll": {} + } + }, + "Newtonsoft.Json/13.0.3": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.3.27908" + } + } + }, + "Microsoft.AspNetCore.DataProtection/10.0.0.0": { + "compile": { + "Microsoft.AspNetCore.DataProtection.dll": {} + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions/10.0.0.0": { + "compile": { + "Microsoft.AspNetCore.DataProtection.Abstractions.dll": {} + } + } + } + }, + "libraries": { + "MyApp/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Newtonsoft.Json/13.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==", + "path": "newtonsoft.json/13.0.3", + "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512" + }, + "Microsoft.AspNetCore.DataProtection/10.0.0.0": { + "type": "referenceassembly", + "serviceable": false, + "sha512": "" + }, + "Microsoft.AspNetCore.DataProtection.Abstractions/10.0.0.0": { + "type": "referenceassembly", + "serviceable": false, + "sha512": "" + } + } +} From 951fbd454abd8e474444b2fc506143a94f4800de Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 16 Jun 2026 12:13:34 -0400 Subject: [PATCH 007/168] add purl types to cataloger info cmd (#4984) Signed-off-by: Alex Goodman --- cmd/syft/internal/commands/cataloger_info.go | 24 +++++++--- .../internal/commands/cataloger_info_test.go | 8 ++++ internal/capabilities/generate/io.go | 7 ++- internal/capabilities/generate/merge.go | 38 ++++++++++++++++ internal/capabilities/generate/merge_test.go | 45 +++++++++++++++++++ internal/capabilities/model.go | 2 + syft/pkg/cataloger/alpine/capabilities.yaml | 2 + syft/pkg/cataloger/arch/capabilities.yaml | 2 + syft/pkg/cataloger/binary/capabilities.yaml | 3 ++ syft/pkg/cataloger/bitnami/capabilities.yaml | 4 ++ syft/pkg/cataloger/conda/capabilities.yaml | 2 + syft/pkg/cataloger/cpp/capabilities.yaml | 6 +++ syft/pkg/cataloger/dart/capabilities.yaml | 4 ++ syft/pkg/cataloger/debian/capabilities.yaml | 4 ++ syft/pkg/cataloger/dotnet/capabilities.yaml | 9 ++++ syft/pkg/cataloger/elixir/capabilities.yaml | 2 + syft/pkg/cataloger/erlang/capabilities.yaml | 4 ++ syft/pkg/cataloger/gentoo/capabilities.yaml | 2 + .../cataloger/githubactions/capabilities.yaml | 6 +++ syft/pkg/cataloger/golang/capabilities.yaml | 4 ++ syft/pkg/cataloger/haskell/capabilities.yaml | 6 +++ syft/pkg/cataloger/homebrew/capabilities.yaml | 2 + syft/pkg/cataloger/java/capabilities.yaml | 6 +++ .../cataloger/javascript/capabilities.yaml | 10 +++++ syft/pkg/cataloger/kernel/capabilities.yaml | 3 ++ syft/pkg/cataloger/lua/capabilities.yaml | 2 + syft/pkg/cataloger/nix/capabilities.yaml | 4 ++ syft/pkg/cataloger/ocaml/capabilities.yaml | 2 + syft/pkg/cataloger/php/capabilities.yaml | 8 ++++ syft/pkg/cataloger/python/capabilities.yaml | 14 ++++++ syft/pkg/cataloger/r/capabilities.yaml | 2 + syft/pkg/cataloger/redhat/capabilities.yaml | 6 +++ syft/pkg/cataloger/ruby/capabilities.yaml | 6 +++ syft/pkg/cataloger/rust/capabilities.yaml | 4 ++ syft/pkg/cataloger/sbom/capabilities.yaml | 2 + syft/pkg/cataloger/snap/capabilities.yaml | 8 ++++ syft/pkg/cataloger/swift/capabilities.yaml | 4 ++ syft/pkg/cataloger/swipl/capabilities.yaml | 2 + .../pkg/cataloger/terraform/capabilities.yaml | 2 + .../pkg/cataloger/wordpress/capabilities.yaml | 2 + 40 files changed, 266 insertions(+), 7 deletions(-) diff --git a/cmd/syft/internal/commands/cataloger_info.go b/cmd/syft/internal/commands/cataloger_info.go index 818d91d5d..57e0f2744 100644 --- a/cmd/syft/internal/commands/cataloger_info.go +++ b/cmd/syft/internal/commands/cataloger_info.go @@ -60,6 +60,7 @@ type ( Packages []detectorPackageInfo `json:"packages,omitempty"` Comment string `json:"comment,omitempty"` PackageTypes []string `json:"package_types,omitempty"` + PURLTypes []string `json:"purl_types,omitempty"` JSONSchemaTypes []string `json:"json_schema_types,omitempty"` Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"` } @@ -235,7 +236,7 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti // if no parsers, use detectors instead if len(info.Patterns) == 0 { info.Capabilities = cat.Capabilities - info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.JSONSchemaTypes) + info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.PURLTypes, cat.JSONSchemaTypes) } info.Config = getConfigInfoFromDocument(doc, cat.Config) @@ -278,6 +279,7 @@ func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo { Packages: convertDetectorPackages(parser.Detector.Packages), Comment: parser.Detector.Comment, PackageTypes: parser.PackageTypes, + PURLTypes: parser.PURLTypes, JSONSchemaTypes: parser.JSONSchemaTypes, Capabilities: parser.Capabilities, }) @@ -286,7 +288,7 @@ func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo { } // convertDetectorsToPatterns converts detector entries to pattern info for JSON output (for non-parser catalogers) -func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, jsonSchemaTypes []string) []patternInfo { +func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, purlTypes, jsonSchemaTypes []string) []patternInfo { var patterns []patternInfo for _, det := range detectors { patterns = append(patterns, patternInfo{ @@ -296,6 +298,7 @@ func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, Packages: convertDetectorPackages(det.Packages), Comment: det.Comment, PackageTypes: packageTypes, + PURLTypes: purlTypes, JSONSchemaTypes: jsonSchemaTypes, }) } @@ -353,7 +356,7 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie ) // set headers - table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH") + table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "PURL", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH") // build rows for each cataloger var data [][]string @@ -367,13 +370,13 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie // generic catalogers: one row per parser for _, parser := range cat.Parsers { criteria := formatCriteria([]capabilities.Detector{parser.Detector}) - row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, parser.Capabilities) + row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, parser.PURLTypes, parser.Capabilities) data = append(data, row) } } else { // custom catalogers: one row with all detectors criteria := formatCriteria(cat.Detectors) - row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.Capabilities) + row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.PURLTypes, cat.Capabilities) data = append(data, row) } } @@ -385,7 +388,7 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie } // buildTableRowFromCapabilities builds a table row from capability values -func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabilities.CapabilitySet) []string { +func buildTableRowFromCapabilities(ecosystem, name, criteria string, purlTypes []string, caps capabilities.CapabilitySet) []string { // extract capability default values license := extractBoolCapability(caps, "license") nodes := extractNodesCapability(caps) @@ -399,6 +402,7 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabi ecosystem, name, criteria, + formatPURLTypes(purlTypes), license, nodes, edges, @@ -409,6 +413,14 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabi } } +// formatPURLTypes renders the PURL types as a comma-separated list, or a placeholder when empty +func formatPURLTypes(purlTypes []string) string { + if len(purlTypes) == 0 { + return noStyle.Render("·") + } + return strings.Join(purlTypes, ", ") +} + // extractBoolCapability extracts a boolean capability value and formats it func extractBoolCapability(caps capabilities.CapabilitySet, name string) string { for _, cap := range caps { diff --git a/cmd/syft/internal/commands/cataloger_info_test.go b/cmd/syft/internal/commands/cataloger_info_test.go index 6794c3b81..9593329f4 100644 --- a/cmd/syft/internal/commands/cataloger_info_test.go +++ b/cmd/syft/internal/commands/cataloger_info_test.go @@ -280,12 +280,15 @@ func Test_catalogerInfoReport(t *testing.T) { Detectors: []capabilities.Detector{ {Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}}, }, + PURLTypes: []string{"npm"}, }, }, assertions: func(t *testing.T, got string) { assert.Contains(t, got, "test-cataloger") assert.Contains(t, got, "ECOSYSTEM") assert.Contains(t, got, "CATALOGER") + assert.Contains(t, got, "PURL") + assert.Contains(t, got, "npm") }, }, { @@ -298,12 +301,17 @@ func Test_catalogerInfoReport(t *testing.T) { Ecosystem: "test", Type: "custom", Selectors: []string{"test", "custom"}, + Detectors: []capabilities.Detector{ + {Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}}, + }, + PURLTypes: []string{"npm"}, }, }, assertions: func(t *testing.T, got string) { assert.Contains(t, got, `"name":"test-cataloger"`) assert.Contains(t, got, `"ecosystem":"test"`) assert.Contains(t, got, `"type":"custom"`) + assert.Contains(t, got, `"purl_types":["npm"]`) }, }, { diff --git a/internal/capabilities/generate/io.go b/internal/capabilities/generate/io.go index 762948618..3d660cf32 100644 --- a/internal/capabilities/generate/io.go +++ b/internal/capabilities/generate/io.go @@ -297,6 +297,11 @@ func addCatalogerFieldComment(keyNode, valueNode *yaml.Node, catalogerName strin if keyNode.LineComment == "" { keyNode.LineComment = autoGeneratedComment } + case "purl_types": + // cataloger-level purl_types (for custom catalogers) are AUTO-GENERATED + if keyNode.LineComment == "" { + keyNode.LineComment = autoGeneratedComment + } case "json_schema_types": // json_schema_types are AUTO-GENERATED if keyNode.LineComment == "" { @@ -367,7 +372,7 @@ func addParserComments(parsersNode *yaml.Node) { valueNode := parserNode.Content[i+1] switch keyNode.Value { - case "parser_function", "metadata_types", "package_types", "json_schema_types": + case "parser_function", "metadata_types", "package_types", "purl_types", "json_schema_types": // add AUTO-GENERATED comment to these fields if keyNode.LineComment == "" { keyNode.LineComment = autoGeneratedComment diff --git a/internal/capabilities/generate/merge.go b/internal/capabilities/generate/merge.go index 5f045fb82..b5d3b9b02 100644 --- a/internal/capabilities/generate/merge.go +++ b/internal/capabilities/generate/merge.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "sort" "strings" "github.com/scylladb/go-set/strset" @@ -10,6 +11,7 @@ import ( "github.com/anchore/syft/internal/capabilities" "github.com/anchore/syft/internal/capabilities/internal" "github.com/anchore/syft/internal/packagemetadata" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/binary" ) @@ -192,6 +194,9 @@ func RegenerateCapabilities(catalogerDir string, repoRoot string) (*Statistics, formatOrphans(orphans)) } + // 5a. Derive PURL types from the discovered package types (purely a function of pkg.Type) + derivePURLTypes(updated) + // 6. Write back to YAML files with comments, preserving existing node trees fmt.Print(" → Writing updated capabilities files...") if err := saveCapabilities(catalogerDir, repoRoot, updated, existingNodes); err != nil { @@ -427,6 +432,39 @@ func (e *EnrichmentData) GetMetadataTypes(catalogerName string) ([]string, bool) return types, ok } +// derivePURLTypes populates the PURLTypes field on every cataloger and parser by mapping each +// observed package type to its canonical PURL type via pkg.Type.PackageURLType. this is a pure +// function of the already-discovered package_types, so it needs no additional test observations. +func derivePURLTypes(doc *capabilities.Document) { + for i := range doc.Catalogers { + cat := &doc.Catalogers[i] + cat.PURLTypes = purlTypesFromPackageTypes(cat.PackageTypes) + for j := range cat.Parsers { + cat.Parsers[j].PURLTypes = purlTypesFromPackageTypes(cat.Parsers[j].PackageTypes) + } + } +} + +// purlTypesFromPackageTypes maps syft package type strings (e.g. "go-module") to their PURL types +// (e.g. "golang"), returning a deduplicated, sorted slice. package types with no PURL mapping are skipped. +func purlTypesFromPackageTypes(packageTypes []string) []string { + if len(packageTypes) == 0 { + return nil + } + set := strset.New() + for _, pt := range packageTypes { + if purlType := pkg.Type(pt).PackageURLType(); purlType != "" { + set.Add(purlType) + } + } + if set.IsEmpty() { + return nil + } + result := set.List() + sort.Strings(result) + return result +} + // GetPackageTypes returns the package types for the given cataloger name func (e *EnrichmentData) GetPackageTypes(catalogerName string) ([]string, bool) { types, ok := e.packageTypes[catalogerName] diff --git a/internal/capabilities/generate/merge_test.go b/internal/capabilities/generate/merge_test.go index 229218e5d..8e604af61 100644 --- a/internal/capabilities/generate/merge_test.go +++ b/internal/capabilities/generate/merge_test.go @@ -552,3 +552,48 @@ func TestConvertToJSONSchemaTypesFromMetadata(t *testing.T) { }) } } + +func TestPURLTypesFromPackageTypes(t *testing.T) { + tests := []struct { + name string + packageTypes []string + want []string + }{ + { + name: "nil slice returns nil", + packageTypes: nil, + want: nil, + }, + { + name: "single package type maps to purl type", + packageTypes: []string{"go-module"}, + want: []string{"golang"}, + }, + { + name: "multiple package types are sorted", + packageTypes: []string{"npm", "go-module"}, + want: []string{"golang", "npm"}, + }, + { + // java and jenkins-plugin both map to maven, so the result is deduplicated + name: "distinct package types collapsing to one purl type are deduplicated", + packageTypes: []string{"java-archive", "jenkins-plugin"}, + want: []string{"maven"}, + }, + { + // unknown package types have no purl mapping and are skipped + name: "package type without a purl mapping is skipped", + packageTypes: []string{"UnknownPackage"}, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := purlTypesFromPackageTypes(tt.packageTypes) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("purlTypesFromPackageTypes() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/capabilities/model.go b/internal/capabilities/model.go index c969897b8..b43790c84 100644 --- a/internal/capabilities/model.go +++ b/internal/capabilities/model.go @@ -82,6 +82,7 @@ type CatalogerEntry struct { Detectors []Detector `yaml:"detectors,omitempty" json:"detectors,omitempty"` // AUTO-GENERATED - detection methods (only for type=custom) MetadataTypes []string `yaml:"metadata_types,omitempty" json:"metadata_types,omitempty"` // AUTO-GENERATED - pkg metadata types emitted (only for type=custom) PackageTypes []string `yaml:"package_types,omitempty" json:"package_types,omitempty"` // AUTO-GENERATED - package types emitted (only for type=custom) + PURLTypes []string `yaml:"purl_types,omitempty" json:"purl_types,omitempty"` // AUTO-GENERATED - PURL types derived from package_types (only for type=custom) JSONSchemaTypes []string `yaml:"json_schema_types,omitempty" json:"json_schema_types,omitempty"` // AUTO-GENERATED - JSON schema type names (UpperCamelCase) Capabilities CapabilitySet `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` // MANUAL - config-driven capability definitions (only for type=custom) } @@ -92,6 +93,7 @@ type Parser struct { Detector Detector `yaml:"detector" json:"detector"` // AUTO-GENERATED - how artifacts are detected MetadataTypes []string `yaml:"metadata_types,omitempty" json:"metadata_types,omitempty"` // AUTO-GENERATED - pkg metadata types emitted by this parser PackageTypes []string `yaml:"package_types,omitempty" json:"package_types,omitempty"` // AUTO-GENERATED - package types emitted by this parser + PURLTypes []string `yaml:"purl_types,omitempty" json:"purl_types,omitempty"` // AUTO-GENERATED - PURL types derived from package_types JSONSchemaTypes []string `yaml:"json_schema_types,omitempty" json:"json_schema_types,omitempty"` // AUTO-GENERATED - JSON schema type names (UpperCamelCase) Capabilities CapabilitySet `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` // MANUAL - config-driven capability definitions } diff --git a/syft/pkg/cataloger/alpine/capabilities.yaml b/syft/pkg/cataloger/alpine/capabilities.yaml index 8f0efe66f..e42f7dd69 100644 --- a/syft/pkg/cataloger/alpine/capabilities.yaml +++ b/syft/pkg/cataloger/alpine/capabilities.yaml @@ -26,6 +26,8 @@ catalogers: - pkg.ApkDBEntry package_types: # AUTO-GENERATED - apk + purl_types: # AUTO-GENERATED + - apk json_schema_types: # AUTO-GENERATED - ApkDbEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/arch/capabilities.yaml b/syft/pkg/cataloger/arch/capabilities.yaml index 1c8c7057b..dfa9c97b1 100644 --- a/syft/pkg/cataloger/arch/capabilities.yaml +++ b/syft/pkg/cataloger/arch/capabilities.yaml @@ -27,6 +27,8 @@ catalogers: - pkg.AlpmDBEntry package_types: # AUTO-GENERATED - alpm + purl_types: # AUTO-GENERATED + - alpm json_schema_types: # AUTO-GENERATED - AlpmDbEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index edee01ef6..d3b959da4 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -932,6 +932,9 @@ catalogers: - apk - binary - rpm + purl_types: # AUTO-GENERATED + - apk + - rpm json_schema_types: # AUTO-GENERATED - ElfBinaryPackageNoteJsonPayload capabilities: # MANUAL - edit capabilities here diff --git a/syft/pkg/cataloger/bitnami/capabilities.yaml b/syft/pkg/cataloger/bitnami/capabilities.yaml index 4edca292e..94ebf49b9 100644 --- a/syft/pkg/cataloger/bitnami/capabilities.yaml +++ b/syft/pkg/cataloger/bitnami/capabilities.yaml @@ -22,6 +22,8 @@ catalogers: - pkg.BitnamiSBOMEntry package_types: # AUTO-GENERATED - bitnami + purl_types: # AUTO-GENERATED + - bitnami json_schema_types: # AUTO-GENERATED - BitnamiSbomEntry capabilities: # MANUAL - preserved across regeneration @@ -51,6 +53,8 @@ catalogers: - pkg.BitnamiSBOMEntry package_types: # AUTO-GENERATED - bitnami + purl_types: # AUTO-GENERATED + - bitnami json_schema_types: # AUTO-GENERATED - BitnamiSbomEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/conda/capabilities.yaml b/syft/pkg/cataloger/conda/capabilities.yaml index 7c306c660..ebb0b0ea3 100644 --- a/syft/pkg/cataloger/conda/capabilities.yaml +++ b/syft/pkg/cataloger/conda/capabilities.yaml @@ -22,6 +22,8 @@ catalogers: - pkg.CondaMetaPackage package_types: # AUTO-GENERATED - conda + purl_types: # AUTO-GENERATED + - generic json_schema_types: # AUTO-GENERATED - CondaMetadataEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/cpp/capabilities.yaml b/syft/pkg/cataloger/cpp/capabilities.yaml index e96431121..2f27f4d7f 100644 --- a/syft/pkg/cataloger/cpp/capabilities.yaml +++ b/syft/pkg/cataloger/cpp/capabilities.yaml @@ -25,6 +25,8 @@ catalogers: - pkg.ConanV2LockEntry package_types: # AUTO-GENERATED - conan + purl_types: # AUTO-GENERATED + - conan json_schema_types: # AUTO-GENERATED - CConanLockEntry - CConanLockV2Entry @@ -59,6 +61,8 @@ catalogers: - pkg.ConanfileEntry package_types: # AUTO-GENERATED - conan + purl_types: # AUTO-GENERATED + - conan json_schema_types: # AUTO-GENERATED - CConanFileEntry capabilities: # MANUAL - preserved across regeneration @@ -101,6 +105,8 @@ catalogers: - pkg.ConaninfoEntry package_types: # AUTO-GENERATED - conan + purl_types: # AUTO-GENERATED + - conan json_schema_types: # AUTO-GENERATED - CConanInfoEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/dart/capabilities.yaml b/syft/pkg/cataloger/dart/capabilities.yaml index cfcf8a0e8..696b1edf1 100644 --- a/syft/pkg/cataloger/dart/capabilities.yaml +++ b/syft/pkg/cataloger/dart/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.DartPubspec package_types: # AUTO-GENERATED - dart-pub + purl_types: # AUTO-GENERATED + - pub json_schema_types: # AUTO-GENERATED - DartPubspec capabilities: # MANUAL - preserved across regeneration @@ -65,6 +67,8 @@ catalogers: - pkg.DartPubspecLockEntry package_types: # AUTO-GENERATED - dart-pub + purl_types: # AUTO-GENERATED + - pub json_schema_types: # AUTO-GENERATED - DartPubspecLockEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/debian/capabilities.yaml b/syft/pkg/cataloger/debian/capabilities.yaml index 248812217..6d1f21e99 100644 --- a/syft/pkg/cataloger/debian/capabilities.yaml +++ b/syft/pkg/cataloger/debian/capabilities.yaml @@ -29,6 +29,8 @@ catalogers: - pkg.DpkgDBEntry package_types: # AUTO-GENERATED - deb + purl_types: # AUTO-GENERATED + - deb json_schema_types: # AUTO-GENERATED - DpkgDbEntry capabilities: # MANUAL - preserved across regeneration @@ -77,6 +79,8 @@ catalogers: - pkg.DpkgArchiveEntry package_types: # AUTO-GENERATED - deb + purl_types: # AUTO-GENERATED + - deb json_schema_types: # AUTO-GENERATED - DpkgArchiveEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/dotnet/capabilities.yaml b/syft/pkg/cataloger/dotnet/capabilities.yaml index 11e537c7c..aa6b9c499 100644 --- a/syft/pkg/cataloger/dotnet/capabilities.yaml +++ b/syft/pkg/cataloger/dotnet/capabilities.yaml @@ -45,6 +45,9 @@ catalogers: package_types: # AUTO-GENERATED - dotnet - npm + purl_types: # AUTO-GENERATED + - dotnet + - npm json_schema_types: # AUTO-GENERATED - DotnetDepsEntry - DotnetPortableExecutableEntry @@ -83,6 +86,8 @@ catalogers: - pkg.DotnetDepsEntry package_types: # AUTO-GENERATED - dotnet + purl_types: # AUTO-GENERATED + - dotnet json_schema_types: # AUTO-GENERATED - DotnetDepsEntry capabilities: # MANUAL - edit capabilities here @@ -127,6 +132,8 @@ catalogers: - pkg.DotnetPackagesLockEntry package_types: # AUTO-GENERATED - dotnet + purl_types: # AUTO-GENERATED + - dotnet json_schema_types: # AUTO-GENERATED - DotnetPackagesLockEntry capabilities: # MANUAL - preserved across regeneration @@ -170,6 +177,8 @@ catalogers: - pkg.DotnetPortableExecutableEntry package_types: # AUTO-GENERATED - dotnet + purl_types: # AUTO-GENERATED + - dotnet json_schema_types: # AUTO-GENERATED - DotnetPortableExecutableEntry capabilities: # MANUAL - edit capabilities here diff --git a/syft/pkg/cataloger/elixir/capabilities.yaml b/syft/pkg/cataloger/elixir/capabilities.yaml index 4ce07a862..bc0dffa09 100644 --- a/syft/pkg/cataloger/elixir/capabilities.yaml +++ b/syft/pkg/cataloger/elixir/capabilities.yaml @@ -23,6 +23,8 @@ catalogers: - pkg.ElixirMixLockEntry package_types: # AUTO-GENERATED - hex + purl_types: # AUTO-GENERATED + - hex json_schema_types: # AUTO-GENERATED - ElixirMixLockEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/erlang/capabilities.yaml b/syft/pkg/cataloger/erlang/capabilities.yaml index b7d490909..342f401f1 100644 --- a/syft/pkg/cataloger/erlang/capabilities.yaml +++ b/syft/pkg/cataloger/erlang/capabilities.yaml @@ -22,6 +22,8 @@ catalogers: - '**/*.app' package_types: # AUTO-GENERATED - erlang-otp + purl_types: # AUTO-GENERATED + - otp capabilities: # MANUAL - preserved across regeneration - name: license default: false @@ -62,6 +64,8 @@ catalogers: - pkg.ErlangRebarLockEntry package_types: # AUTO-GENERATED - hex + purl_types: # AUTO-GENERATED + - hex json_schema_types: # AUTO-GENERATED - ErlangRebarLockEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/gentoo/capabilities.yaml b/syft/pkg/cataloger/gentoo/capabilities.yaml index 5b8745c36..aaed8063b 100644 --- a/syft/pkg/cataloger/gentoo/capabilities.yaml +++ b/syft/pkg/cataloger/gentoo/capabilities.yaml @@ -26,6 +26,8 @@ catalogers: - pkg.PortageEntry package_types: # AUTO-GENERATED - portage + purl_types: # AUTO-GENERATED + - portage json_schema_types: # AUTO-GENERATED - PortageDbEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/githubactions/capabilities.yaml b/syft/pkg/cataloger/githubactions/capabilities.yaml index 4ff9d032d..67719dd63 100644 --- a/syft/pkg/cataloger/githubactions/capabilities.yaml +++ b/syft/pkg/cataloger/githubactions/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.GitHubActionsUseStatement package_types: # AUTO-GENERATED - github-action-workflow + purl_types: # AUTO-GENERATED + - github json_schema_types: # AUTO-GENERATED - GithubActionsUseStatement capabilities: # MANUAL - preserved across regeneration @@ -64,6 +66,8 @@ catalogers: - pkg.GitHubActionsUseStatement package_types: # AUTO-GENERATED - github-action + purl_types: # AUTO-GENERATED + - github json_schema_types: # AUTO-GENERATED - GithubActionsUseStatement capabilities: # MANUAL - preserved across regeneration @@ -91,6 +95,8 @@ catalogers: - pkg.GitHubActionsUseStatement package_types: # AUTO-GENERATED - github-action + purl_types: # AUTO-GENERATED + - github json_schema_types: # AUTO-GENERATED - GithubActionsUseStatement capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/golang/capabilities.yaml b/syft/pkg/cataloger/golang/capabilities.yaml index 49e87f1a3..b0e55805c 100644 --- a/syft/pkg/cataloger/golang/capabilities.yaml +++ b/syft/pkg/cataloger/golang/capabilities.yaml @@ -57,6 +57,8 @@ catalogers: - pkg.GolangBinaryBuildinfoEntry package_types: # AUTO-GENERATED - go-module + purl_types: # AUTO-GENERATED + - golang json_schema_types: # AUTO-GENERATED - GoModuleBuildinfoEntry capabilities: # MANUAL - preserved across regeneration @@ -113,6 +115,8 @@ catalogers: - pkg.GolangSourceEntry package_types: # AUTO-GENERATED - go-module + purl_types: # AUTO-GENERATED + - golang json_schema_types: # AUTO-GENERATED - GoModuleEntry - GoSourceEntry diff --git a/syft/pkg/cataloger/haskell/capabilities.yaml b/syft/pkg/cataloger/haskell/capabilities.yaml index 0c1de7b1b..25ea697ac 100644 --- a/syft/pkg/cataloger/haskell/capabilities.yaml +++ b/syft/pkg/cataloger/haskell/capabilities.yaml @@ -23,6 +23,8 @@ catalogers: - '**/cabal.project.freeze' package_types: # AUTO-GENERATED - hackage + purl_types: # AUTO-GENERATED + - hackage capabilities: # MANUAL - preserved across regeneration - name: license default: false @@ -51,6 +53,8 @@ catalogers: - pkg.HackageStackYamlLockEntry package_types: # AUTO-GENERATED - hackage + purl_types: # AUTO-GENERATED + - hackage json_schema_types: # AUTO-GENERATED - HaskellHackageStackLockEntry capabilities: # MANUAL - preserved across regeneration @@ -83,6 +87,8 @@ catalogers: - pkg.HackageStackYamlEntry package_types: # AUTO-GENERATED - hackage + purl_types: # AUTO-GENERATED + - hackage json_schema_types: # AUTO-GENERATED - HaskellHackageStackEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/homebrew/capabilities.yaml b/syft/pkg/cataloger/homebrew/capabilities.yaml index fb7ac85f8..ea9b2b08a 100644 --- a/syft/pkg/cataloger/homebrew/capabilities.yaml +++ b/syft/pkg/cataloger/homebrew/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.HomebrewFormula package_types: # AUTO-GENERATED - homebrew + purl_types: # AUTO-GENERATED + - homebrew json_schema_types: # AUTO-GENERATED - HomebrewFormula capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/java/capabilities.yaml b/syft/pkg/cataloger/java/capabilities.yaml index 06c5526b9..84469f497 100644 --- a/syft/pkg/cataloger/java/capabilities.yaml +++ b/syft/pkg/cataloger/java/capabilities.yaml @@ -91,6 +91,8 @@ catalogers: - pkg.JavaArchive package_types: # AUTO-GENERATED - java-archive + purl_types: # AUTO-GENERATED + - maven json_schema_types: # AUTO-GENERATED - JavaArchive capabilities: # MANUAL - edit capabilities here @@ -137,6 +139,8 @@ catalogers: - pkg.JavaArchive package_types: # AUTO-GENERATED - java-archive + purl_types: # AUTO-GENERATED + - maven json_schema_types: # AUTO-GENERATED - JavaArchive capabilities: # MANUAL - preserved across regeneration @@ -179,6 +183,8 @@ catalogers: - pkg.JavaArchive package_types: # AUTO-GENERATED - java-archive + purl_types: # AUTO-GENERATED + - maven json_schema_types: # AUTO-GENERATED - JavaArchive capabilities: # MANUAL - edit capabilities here diff --git a/syft/pkg/cataloger/javascript/capabilities.yaml b/syft/pkg/cataloger/javascript/capabilities.yaml index 3a11c74a1..0cec8d141 100644 --- a/syft/pkg/cataloger/javascript/capabilities.yaml +++ b/syft/pkg/cataloger/javascript/capabilities.yaml @@ -38,6 +38,8 @@ catalogers: - pkg.BunLockEntry package_types: # AUTO-GENERATED - npm + purl_types: # AUTO-GENERATED + - npm json_schema_types: # AUTO-GENERATED - JavascriptBunLockEntry capabilities: # MANUAL - preserved across regeneration @@ -70,6 +72,8 @@ catalogers: - pkg.PnpmLockEntry package_types: # AUTO-GENERATED - npm + purl_types: # AUTO-GENERATED + - npm json_schema_types: # AUTO-GENERATED - JavascriptPnpmLockEntry capabilities: # MANUAL - preserved across regeneration @@ -99,6 +103,8 @@ catalogers: - pkg.YarnLockEntry package_types: # AUTO-GENERATED - npm + purl_types: # AUTO-GENERATED + - npm json_schema_types: # AUTO-GENERATED - JavascriptYarnLockEntry capabilities: # MANUAL - preserved across regeneration @@ -131,6 +137,8 @@ catalogers: - pkg.NpmPackageLockEntry package_types: # AUTO-GENERATED - npm + purl_types: # AUTO-GENERATED + - npm json_schema_types: # AUTO-GENERATED - JavascriptNpmPackageLockEntry capabilities: # MANUAL - preserved across regeneration @@ -177,6 +185,8 @@ catalogers: - pkg.NpmPackage package_types: # AUTO-GENERATED - npm + purl_types: # AUTO-GENERATED + - npm json_schema_types: # AUTO-GENERATED - JavascriptNpmPackage capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/kernel/capabilities.yaml b/syft/pkg/cataloger/kernel/capabilities.yaml index c6e8afe1e..f24b3e8e1 100644 --- a/syft/pkg/cataloger/kernel/capabilities.yaml +++ b/syft/pkg/cataloger/kernel/capabilities.yaml @@ -45,6 +45,9 @@ catalogers: package_types: # AUTO-GENERATED - linux-kernel - linux-kernel-module + purl_types: # AUTO-GENERATED + - generic + - generic/linux-kernel json_schema_types: # AUTO-GENERATED - LinuxKernelArchive - LinuxKernelModule diff --git a/syft/pkg/cataloger/lua/capabilities.yaml b/syft/pkg/cataloger/lua/capabilities.yaml index edbdcf894..a8f5edc08 100644 --- a/syft/pkg/cataloger/lua/capabilities.yaml +++ b/syft/pkg/cataloger/lua/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.LuaRocksPackage package_types: # AUTO-GENERATED - lua-rocks + purl_types: # AUTO-GENERATED + - luarocks json_schema_types: # AUTO-GENERATED - LuarocksPackage capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/nix/capabilities.yaml b/syft/pkg/cataloger/nix/capabilities.yaml index 6df5d58f4..c919b1679 100644 --- a/syft/pkg/cataloger/nix/capabilities.yaml +++ b/syft/pkg/cataloger/nix/capabilities.yaml @@ -30,6 +30,8 @@ catalogers: - pkg.NixStoreEntry package_types: # AUTO-GENERATED - nix + purl_types: # AUTO-GENERATED + - nix json_schema_types: # AUTO-GENERATED - NixStoreEntry capabilities: # MANUAL - edit capabilities here @@ -71,6 +73,8 @@ catalogers: - pkg.NixStoreEntry package_types: # AUTO-GENERATED - nix + purl_types: # AUTO-GENERATED + - nix json_schema_types: # AUTO-GENERATED - NixStoreEntry capabilities: # MANUAL - edit capabilities here diff --git a/syft/pkg/cataloger/ocaml/capabilities.yaml b/syft/pkg/cataloger/ocaml/capabilities.yaml index b49a98a14..541944fc3 100644 --- a/syft/pkg/cataloger/ocaml/capabilities.yaml +++ b/syft/pkg/cataloger/ocaml/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.OpamPackage package_types: # AUTO-GENERATED - opam + purl_types: # AUTO-GENERATED + - opam json_schema_types: # AUTO-GENERATED - OpamPackage capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/php/capabilities.yaml b/syft/pkg/cataloger/php/capabilities.yaml index 7fe336c05..d448221a2 100644 --- a/syft/pkg/cataloger/php/capabilities.yaml +++ b/syft/pkg/cataloger/php/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.PhpComposerInstalledEntry package_types: # AUTO-GENERATED - php-composer + purl_types: # AUTO-GENERATED + - composer json_schema_types: # AUTO-GENERATED - PhpComposerInstalledEntry capabilities: # MANUAL - preserved across regeneration @@ -68,6 +70,8 @@ catalogers: - pkg.PhpComposerLockEntry package_types: # AUTO-GENERATED - php-composer + purl_types: # AUTO-GENERATED + - composer json_schema_types: # AUTO-GENERATED - PhpComposerLockEntry capabilities: # MANUAL - preserved across regeneration @@ -156,6 +160,8 @@ catalogers: - pkg.PhpPearEntry package_types: # AUTO-GENERATED - php-pear + purl_types: # AUTO-GENERATED + - pear json_schema_types: # AUTO-GENERATED - PhpPearEntry capabilities: # MANUAL - preserved across regeneration @@ -194,6 +200,8 @@ catalogers: - pkg.PhpPeclEntry package_types: # AUTO-GENERATED - php-pecl + purl_types: # AUTO-GENERATED + - pear json_schema_types: # AUTO-GENERATED - PhpPeclEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/python/capabilities.yaml b/syft/pkg/cataloger/python/capabilities.yaml index dfbadc272..c2b403b26 100644 --- a/syft/pkg/cataloger/python/capabilities.yaml +++ b/syft/pkg/cataloger/python/capabilities.yaml @@ -40,6 +40,8 @@ catalogers: - pkg.PythonPackage package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi json_schema_types: # AUTO-GENERATED - PythonPackage capabilities: # MANUAL - preserved across regeneration @@ -86,6 +88,8 @@ catalogers: - pkg.PythonPdmLockEntry package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi json_schema_types: # AUTO-GENERATED - PythonPdmLockEntry capabilities: # MANUAL - preserved across regeneration @@ -118,6 +122,8 @@ catalogers: - pkg.PythonUvLockEntry package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi json_schema_types: # AUTO-GENERATED - PythonUvLockEntry capabilities: # MANUAL - preserved across regeneration @@ -147,6 +153,8 @@ catalogers: - '**/setup.py' package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi capabilities: # MANUAL - preserved across regeneration - name: license default: false @@ -173,6 +181,8 @@ catalogers: - pkg.PythonPipfileLockEntry package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi json_schema_types: # AUTO-GENERATED - PythonPipfileLockEntry capabilities: # MANUAL - preserved across regeneration @@ -204,6 +214,8 @@ catalogers: - pkg.PythonPoetryLockEntry package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi json_schema_types: # AUTO-GENERATED - PythonPoetryLockEntry capabilities: # MANUAL - preserved across regeneration @@ -235,6 +247,8 @@ catalogers: - pkg.PythonRequirementsEntry package_types: # AUTO-GENERATED - python + purl_types: # AUTO-GENERATED + - pypi json_schema_types: # AUTO-GENERATED - PythonPipRequirementsEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/r/capabilities.yaml b/syft/pkg/cataloger/r/capabilities.yaml index b890f082c..737047ba0 100644 --- a/syft/pkg/cataloger/r/capabilities.yaml +++ b/syft/pkg/cataloger/r/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.RDescription package_types: # AUTO-GENERATED - R-package + purl_types: # AUTO-GENERATED + - cran json_schema_types: # AUTO-GENERATED - RDescription capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/redhat/capabilities.yaml b/syft/pkg/cataloger/redhat/capabilities.yaml index 2480296a0..b6ee9e4fa 100644 --- a/syft/pkg/cataloger/redhat/capabilities.yaml +++ b/syft/pkg/cataloger/redhat/capabilities.yaml @@ -25,6 +25,8 @@ catalogers: - pkg.RpmArchive package_types: # AUTO-GENERATED - rpm + purl_types: # AUTO-GENERATED + - rpm json_schema_types: # AUTO-GENERATED - RpmArchive capabilities: # MANUAL - preserved across regeneration @@ -71,6 +73,8 @@ catalogers: - pkg.RpmDBEntry package_types: # AUTO-GENERATED - rpm + purl_types: # AUTO-GENERATED + - rpm json_schema_types: # AUTO-GENERATED - RpmDbEntry capabilities: # MANUAL - preserved across regeneration @@ -100,6 +104,8 @@ catalogers: - pkg.RpmDBEntry package_types: # AUTO-GENERATED - rpm + purl_types: # AUTO-GENERATED + - rpm json_schema_types: # AUTO-GENERATED - RpmDbEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/ruby/capabilities.yaml b/syft/pkg/cataloger/ruby/capabilities.yaml index 991596552..28edc114b 100644 --- a/syft/pkg/cataloger/ruby/capabilities.yaml +++ b/syft/pkg/cataloger/ruby/capabilities.yaml @@ -23,6 +23,8 @@ catalogers: - '**/Gemfile.next.lock' package_types: # AUTO-GENERATED - gem + purl_types: # AUTO-GENERATED + - gem capabilities: # MANUAL - preserved across regeneration - name: license default: false @@ -66,6 +68,8 @@ catalogers: - pkg.RubyGemspec package_types: # AUTO-GENERATED - gem + purl_types: # AUTO-GENERATED + - gem json_schema_types: # AUTO-GENERATED - RubyGemspec capabilities: # MANUAL - preserved across regeneration @@ -109,6 +113,8 @@ catalogers: - pkg.RubyGemspec package_types: # AUTO-GENERATED - gem + purl_types: # AUTO-GENERATED + - gem json_schema_types: # AUTO-GENERATED - RubyGemspec capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/rust/capabilities.yaml b/syft/pkg/cataloger/rust/capabilities.yaml index 82d211be6..04fc597e8 100644 --- a/syft/pkg/cataloger/rust/capabilities.yaml +++ b/syft/pkg/cataloger/rust/capabilities.yaml @@ -30,6 +30,8 @@ catalogers: - pkg.RustBinaryAuditEntry package_types: # AUTO-GENERATED - rust-crate + purl_types: # AUTO-GENERATED + - cargo json_schema_types: # AUTO-GENERATED - RustCargoAuditEntry capabilities: # MANUAL - preserved across regeneration @@ -73,6 +75,8 @@ catalogers: - pkg.RustCargoLockEntry package_types: # AUTO-GENERATED - rust-crate + purl_types: # AUTO-GENERATED + - cargo json_schema_types: # AUTO-GENERATED - RustCargoLockEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/sbom/capabilities.yaml b/syft/pkg/cataloger/sbom/capabilities.yaml index 5fe58eb18..177cd8f9c 100644 --- a/syft/pkg/cataloger/sbom/capabilities.yaml +++ b/syft/pkg/cataloger/sbom/capabilities.yaml @@ -30,6 +30,8 @@ catalogers: - pkg.ApkDBEntry package_types: # AUTO-GENERATED - apk + purl_types: # AUTO-GENERATED + - apk json_schema_types: # AUTO-GENERATED - ApkDbEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/snap/capabilities.yaml b/syft/pkg/cataloger/snap/capabilities.yaml index b414044e0..2d0789e05 100644 --- a/syft/pkg/cataloger/snap/capabilities.yaml +++ b/syft/pkg/cataloger/snap/capabilities.yaml @@ -23,6 +23,8 @@ catalogers: - pkg.SnapEntry package_types: # AUTO-GENERATED - deb + purl_types: # AUTO-GENERATED + - deb json_schema_types: # AUTO-GENERATED - SnapEntry capabilities: # MANUAL - preserved across regeneration @@ -49,6 +51,8 @@ catalogers: - pkg.SnapEntry package_types: # AUTO-GENERATED - deb + purl_types: # AUTO-GENERATED + - deb json_schema_types: # AUTO-GENERATED - SnapEntry capabilities: # MANUAL - preserved across regeneration @@ -75,6 +79,8 @@ catalogers: - pkg.SnapEntry package_types: # AUTO-GENERATED - deb + purl_types: # AUTO-GENERATED + - deb json_schema_types: # AUTO-GENERATED - SnapEntry capabilities: # MANUAL - preserved across regeneration @@ -101,6 +107,8 @@ catalogers: - pkg.SnapEntry package_types: # AUTO-GENERATED - deb + purl_types: # AUTO-GENERATED + - deb json_schema_types: # AUTO-GENERATED - SnapEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/swift/capabilities.yaml b/syft/pkg/cataloger/swift/capabilities.yaml index b03352c59..182803d6d 100644 --- a/syft/pkg/cataloger/swift/capabilities.yaml +++ b/syft/pkg/cataloger/swift/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.CocoaPodfileLockEntry package_types: # AUTO-GENERATED - pod + purl_types: # AUTO-GENERATED + - cocoapods json_schema_types: # AUTO-GENERATED - CocoaPodfileLockEntry capabilities: # MANUAL - preserved across regeneration @@ -70,6 +72,8 @@ catalogers: - pkg.SwiftPackageManagerResolvedEntry package_types: # AUTO-GENERATED - swift + purl_types: # AUTO-GENERATED + - swift json_schema_types: # AUTO-GENERATED - SwiftPackageManagerLockEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/swipl/capabilities.yaml b/syft/pkg/cataloger/swipl/capabilities.yaml index f88328567..82ac70b73 100644 --- a/syft/pkg/cataloger/swipl/capabilities.yaml +++ b/syft/pkg/cataloger/swipl/capabilities.yaml @@ -24,6 +24,8 @@ catalogers: - pkg.SwiplPackEntry package_types: # AUTO-GENERATED - swiplpack + purl_types: # AUTO-GENERATED + - swiplpack json_schema_types: # AUTO-GENERATED - SwiplpackPackage capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/terraform/capabilities.yaml b/syft/pkg/cataloger/terraform/capabilities.yaml index 427cb5f5d..586a153bd 100644 --- a/syft/pkg/cataloger/terraform/capabilities.yaml +++ b/syft/pkg/cataloger/terraform/capabilities.yaml @@ -22,6 +22,8 @@ catalogers: - pkg.TerraformLockProviderEntry package_types: # AUTO-GENERATED - terraform + purl_types: # AUTO-GENERATED + - terraform json_schema_types: # AUTO-GENERATED - TerraformLockProviderEntry capabilities: # MANUAL - preserved across regeneration diff --git a/syft/pkg/cataloger/wordpress/capabilities.yaml b/syft/pkg/cataloger/wordpress/capabilities.yaml index 9da6257a3..81e5e12f7 100644 --- a/syft/pkg/cataloger/wordpress/capabilities.yaml +++ b/syft/pkg/cataloger/wordpress/capabilities.yaml @@ -22,6 +22,8 @@ catalogers: - pkg.WordpressPluginEntry package_types: # AUTO-GENERATED - wordpress-plugin + purl_types: # AUTO-GENERATED + - wordpress-plugin json_schema_types: # AUTO-GENERATED - WordpressPluginEntry capabilities: # MANUAL - preserved across regeneration From b70fa899cb02cc1b44ca5b757004da7f0c2c577f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 17 Jun 2026 16:34:22 +0200 Subject: [PATCH 008/168] golangci-lint: enable gci formatter (#4828) This allows linting the imports to be grouped correctly, and provides an auto-fix (`golangci-lint run --fix`). Signed-off-by: Sebastiaan van Stijn --- .golangci.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 9093acb66..422a7448a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -70,7 +70,6 @@ linters: # - godot # - godox # - goerr113 -# - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818) # - golint # deprecated # - gomnd # this is too aggressive # - interfacer # this is a good idea, but is no longer supported and is prone to false positives @@ -96,11 +95,18 @@ issues: formatters: enable: + - gci - gofmt - - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ + settings: + gci: + # See https://golangci-lint.run/docs/formatters/configuration/#gci + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/anchore) From 58e4dbbf016bc4e03b08ed55da369186fa336c26 Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Wed, 17 Jun 2026 11:29:07 -0400 Subject: [PATCH 009/168] feat: added bin classifier elastic-agent (#4968) Signed-off-by: Rez Moss --- syft/pkg/cataloger/binary/capabilities.yaml | 10 +++++ .../binary/classifier_cataloger_test.go | 44 +++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 15 +++++++ .../8.11.2/linux-amd64/elastic-agent | 9 ++++ .../8.19.4/linux-amd64/elastic-agent | 8 ++++ .../9.0.0/linux-amd64/elastic-agent | 8 ++++ .../9.4.2/linux-amd64/elastic-agent | 8 ++++ .../pkg/cataloger/binary/testdata/config.yaml | 32 ++++++++++++++ 8 files changed, 134 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.11.2/linux-amd64/elastic-agent create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.19.4/linux-amd64/elastic-agent create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.0.0/linux-amd64/elastic-agent create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.4.2/linux-amd64/elastic-agent diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index d3b959da4..bf7e11d04 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -786,6 +786,16 @@ catalogers: cpes: - cpe:2.3:a:kubernetes:ingress-nginx:*:*:*:*:*:*:*:* type: BinaryPkg + - method: glob + criteria: + - '**/elastic-agent' + packages: + - class: elastic-agent-binary + name: elastic-agent + purl: pkg:generic/elastic-agent + cpes: + - cpe:2.3:a:elastic:elastic_agent:*:*:*:*:*:*:*:* + type: BinaryPkg - method: glob criteria: - '**/java' diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index f8a4beabc..f58963db8 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -2557,6 +2557,50 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("ingress-nginx-binary"), }, }, + { + logicalFixture: "elastic-agent/9.4.2/linux-amd64", + expected: pkg.Package{ + Name: "elastic-agent", + Version: "9.4.2", + Type: "binary", + PURL: "pkg:generic/elastic-agent@9.4.2", + Locations: locations("elastic-agent"), + Metadata: metadata("elastic-agent-binary"), + }, + }, + { + logicalFixture: "elastic-agent/9.0.0/linux-amd64", + expected: pkg.Package{ + Name: "elastic-agent", + Version: "9.0.0", + Type: "binary", + PURL: "pkg:generic/elastic-agent@9.0.0", + Locations: locations("elastic-agent"), + Metadata: metadata("elastic-agent-binary"), + }, + }, + { + logicalFixture: "elastic-agent/8.19.4/linux-amd64", + expected: pkg.Package{ + Name: "elastic-agent", + Version: "8.19.4", + Type: "binary", + PURL: "pkg:generic/elastic-agent@8.19.4", + Locations: locations("elastic-agent"), + Metadata: metadata("elastic-agent-binary"), + }, + }, + { + logicalFixture: "elastic-agent/8.11.2/linux-amd64", + expected: pkg.Package{ + Name: "elastic-agent", + Version: "8.11.2", + Type: "binary", + PURL: "pkg:generic/elastic-agent@8.11.2", + Locations: locations("elastic-agent"), + Metadata: metadata("elastic-agent-binary"), + }, + }, { logicalFixture: "julia/1.13.0-alpha2/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 24280a53c..9b7680a4d 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -1110,6 +1110,21 @@ func DefaultClassifiers() []binutils.Classifier { PURL: mustPURL("pkg:generic/nginx-ingress-controller@version"), CPEs: singleCPE("cpe:2.3:a:kubernetes:ingress-nginx:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, + { + Class: "elastic-agent-binary", + FileGlob: "**/elastic-agent", + EvidenceMatcher: binutils.MatchAny( + // 9.4.x: config/statsenroll: true9.4.2-headeruint16secret + // 9.0.x: configenroll9.0.0-headeruint16secret + // 8.19.x: config/statsenroll8.19.4headeruint16secret + m.FileContentsVersionMatcher(`enroll(?:: true)?(?P[0-9]+\.[0-9]+\.[0-9]+)-?header`), + // 8.11.x: 3:04PM8.11.2:https + m.FileContentsVersionMatcher(`PM(?P[0-9]+\.[0-9]+\.[0-9]+):https`), + ), + Package: "elastic-agent", + PURL: mustPURL("pkg:generic/elastic-agent@version"), + CPEs: singleCPE("cpe:2.3:a:elastic:elastic_agent:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, } return append(classifiers, defaultJavaClassifiers()...) diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.11.2/linux-amd64/elastic-agent b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.11.2/linux-amd64/elastic-agent new file mode 100644 index 000000000..e8a95f893 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.11.2/linux-amd64/elastic-agent @@ -0,0 +1,9 @@ +name: elastic-agent +offset: 29510673 +length: 100 +snippetSha256: abd2e0c395a14431231a89bc9fb4d8cbab5843e3b92b44b4b9c2c942bfedb386 +fileSha256: ff6758786d60089bda6113e302257ae227ab3f15924a3b3856a6a40a7ac5ca88 + +### byte snippet to follow ### +15.01.58.33906253:04PM8.11.2:https<-chan. +AElig;ALLOCSAacuteAcceptAcirc;AgraveAlpha;Amacr; \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.19.4/linux-amd64/elastic-agent b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.19.4/linux-amd64/elastic-agent new file mode 100644 index 000000000..d70f72b93 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/8.19.4/linux-amd64/elastic-agent @@ -0,0 +1,8 @@ +name: elastic-agent +offset: 345016607 +length: 100 +snippetSha256: f66476e828071ae2211f9b279d18a4b23c247bd52cb2bc86cfbe611ec48c88e4 +fileSha256: c7d7bcf1e43ede7b8c5e55537b382ec02b4d8d1493eedb072bfd5f180442ee43 + +### byte snippet to follow ### +atspliceconfig/statsenroll8.19.4headeruint16secret%s: %sinputseventsfollownumbersourcereexecstatusou \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.0.0/linux-amd64/elastic-agent b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.0.0/linux-amd64/elastic-agent new file mode 100644 index 000000000..17dca5fa5 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.0.0/linux-amd64/elastic-agent @@ -0,0 +1,8 @@ +name: elastic-agent +offset: 248594563 +length: 100 +snippetSha256: f1a35077f386583fec63cfbffe42c8eb1a1b59056e5e3a1b15c50afcce0e4d38 +fileSha256: e87819df6fc5a387b42436bd304f4c7d778c1d66c8dbbf4f0aa801e8aa89b596 + +### byte snippet to follow ### +idopenatspliceconfigenroll9.0.0-headeruint16secret%s: %sinputseventsfollownumbersourcereexecstatusou \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.4.2/linux-amd64/elastic-agent b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.4.2/linux-amd64/elastic-agent new file mode 100644 index 000000000..0d2f4081d --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/elastic-agent/9.4.2/linux-amd64/elastic-agent @@ -0,0 +1,8 @@ +name: elastic-agent +offset: 66470886 +length: 100 +snippetSha256: 7cbc4bee88dbcb215e1335dacc614041aa63bca2c5cc4ce05c795f421ce9b143 +fileSha256: 71b8b1552629c1c845516580054eb4d8a1eb1afc1f6fefbcee1922c932fac6a6 + +### byte snippet to follow ### +idspliceconfig/statsenroll: true9.4.2-headeruint16secret%s: %seventsfollownumberreexecstatusoutputso \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml index e1603524b..e27e22e37 100644 --- a/syft/pkg/cataloger/binary/testdata/config.yaml +++ b/syft/pkg/cataloger/binary/testdata/config.yaml @@ -1649,3 +1649,35 @@ from-images: - /usr/local/julia/lib/libjulia.so.1 - /usr/local/julia/lib/libjulia.so.1.3 + + - name: elastic-agent + version: 9.4.2 + images: + - ref: docker.elastic.co/elastic-agent/elastic-agent:9.4.2@sha256:8187c0e2eb4db1a9780838789462bd3ecbeca36fccb89f872258a4f0d3d25ea1 + platform: linux/amd64 + paths: + - /usr/share/elastic-agent/data/elastic-agent-dd9ee6/elastic-agent + + - name: elastic-agent + version: 9.0.0 + images: + - ref: docker.elastic.co/elastic-agent/elastic-agent:9.0.0@sha256:badb97acaf487273298e7f25d21177442d632f63cb9bab6f4defe341612bca07 + platform: linux/amd64 + paths: + - /usr/share/elastic-agent/data/elastic-agent-9786ac/elastic-agent + + - name: elastic-agent + version: 8.19.4 + images: + - ref: docker.elastic.co/elastic-agent/elastic-agent:8.19.4@sha256:b54f796e43941da8665b83d3f31973fc66ef7fc33b43edc4593671325841d8fb + platform: linux/amd64 + paths: + - /usr/share/elastic-agent/data/elastic-agent-8fbe2b/elastic-agent + + - name: elastic-agent + version: 8.11.2 + images: + - ref: docker.elastic.co/elastic-agent/elastic-agent:8.11.2@sha256:1177eb349365132409df73e4a7fa97c32242db2b2c8704b9843d726837638001 + platform: linux/amd64 + paths: + - /usr/share/elastic-agent/data/elastic-agent-1c21b0/elastic-agent \ No newline at end of file From efe3174b5fc43f389f3dfe356ee7bd31abae997f Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 18 Jun 2026 19:50:30 -0400 Subject: [PATCH 010/168] Preserve dependency edges when a compliance stub changes a package ID (#4993) * fix relationship rewrites for isolated nodes Signed-off-by: Alex Goodman * cover dangling pointers Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- internal/relationship/index.go | 52 +++++++++++- internal/relationship/index_test.go | 126 ++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/internal/relationship/index.go b/internal/relationship/index.go index 39aee4ec1..1ce48b69a 100644 --- a/internal/relationship/index.go +++ b/internal/relationship/index.go @@ -65,6 +65,30 @@ func (i *Index) Add(relationships ...artifact.Relationship) { } func (i *Index) Remove(id artifact.ID) { + // scrub this node's edges from the adjacent nodes' maps before dropping the node itself. without this, the + // edges live on as dangling pointers inside the other endpoints' mappedRelationships, so query methods + // (From/To/References/Coordinates/Contains) would keep returning relationships that have been removed. + if mapped := i.fromID[id]; mapped != nil { + for _, sr := range mapped.allRelated { + if sr.to == id { + continue // self-edge: this node's own maps are deleted below + } + if other := i.toID[sr.to]; other != nil { + other.remove(id, sr) + } + } + } + if mapped := i.toID[id]; mapped != nil { + for _, sr := range mapped.allRelated { + if sr.from == id { + continue // self-edge: this node's own maps are deleted below + } + if other := i.fromID[sr.from]; other != nil { + other.remove(id, sr) + } + } + } + delete(i.fromID, id) delete(i.toID, id) @@ -78,6 +102,12 @@ func (i *Index) Remove(id artifact.ID) { } func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) { + if replacement == nil || replacement.ID() == ogID { + // replacing a node with one that has the same ID would re-add the (deduped) edges and then the trailing + // Remove(ogID) would delete them, silently wiping all of the node's relationships. treat it as a no-op. + return + } + for _, mapped := range fromMappedByID(i.fromID, ogID) { // the stale relationship(i.e. if there's an elder ID in either side) should be discarded if len(fromMappedByID(i.toID, mapped.relationship.To.ID())) == 0 { @@ -91,8 +121,10 @@ func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) { } for _, mapped := range fromMappedByID(i.toID, ogID) { - // same as the above - if len(fromMappedByID(i.fromID, mapped.relationship.To.ID())) == 0 { + // same as the above, but check the surviving other endpoint (the From side, since these are edges TO ogID). + // note: this must reference From (not To, which is ogID itself) so that nodes appearing only on the To side + // of relationships (e.g. a go main module that nothing depends on) keep their edges after an ID change. + if len(fromMappedByID(i.fromID, mapped.relationship.From.ID())) == 0 { continue } i.Add(artifact.Relationship{ @@ -206,6 +238,22 @@ func (m *mappedRelationships) add(id artifact.ID, newRelationship *sortableRelat typeMap[id] = newRelationship } +// remove deletes a single relationship (the other endpoint keyed by id) from this node's maps. it is the inverse +// of add and is used to keep adjacent nodes consistent when a node is removed from the index. +func (m *mappedRelationships) remove(id artifact.ID, target *sortableRelationship) { + filtered := m.allRelated[:0] + for _, r := range m.allRelated { + if r != target { + filtered = append(filtered, r) + } + } + m.allRelated = filtered + + if typeMap := m.typeMap[target.relationship.Type]; typeMap != nil && typeMap[id] == target { + delete(typeMap, id) + } +} + type sortableRelationship struct { from artifact.ID to artifact.ID diff --git a/internal/relationship/index_test.go b/internal/relationship/index_test.go index 8f0d010f2..12c933346 100644 --- a/internal/relationship/index_test.go +++ b/internal/relationship/index_test.go @@ -380,6 +380,41 @@ func TestReplace(t *testing.T) { compareRelationships(t, expectedRels, index.All()) } +func TestReplace_NodeOnlyOnToSide(t *testing.T) { + // regression: a node that only appears on the To side of relationships (nothing depends on it, e.g. a go main + // module that other modules are a dependency-of) must keep its incoming edges when its ID changes (such as when a + // compliance rule stubs a missing version and recomputes the package ID). previously the To-side remap guard + // checked ogID itself instead of the surviving From endpoint, so all such edges were dropped. + main := pkg.Package{Name: "main-module"} // empty version, like a go main module + dep1 := pkg.Package{Name: "dep-1", Version: "1.0.0"} + dep2 := pkg.Package{Name: "dep-2", Version: "2.0.0"} + + for _, p := range []*pkg.Package{&main, &dep1, &dep2} { + p.SetID() + } + + // both deps are a dependency-of main; main has no outgoing edges + r1 := artifact.Relationship{From: dep1, To: main, Type: artifact.DependencyOfRelationship} + r2 := artifact.Relationship{From: dep2, To: main, Type: artifact.DependencyOfRelationship} + + index := NewIndex(r1, r2) + + // simulate the version stub: main gets a new ID + stubbedMain := main + stubbedMain.Version = "UNKNOWN" + stubbedMain.SetID() + require.NotEqual(t, main.ID(), stubbedMain.ID()) + + index.Replace(main.ID(), &stubbedMain) + + expectedRels := []artifact.Relationship{ + {From: dep1, To: stubbedMain, Type: artifact.DependencyOfRelationship}, + {From: dep2, To: stubbedMain, Type: artifact.DependencyOfRelationship}, + } + + compareRelationships(t, expectedRels, index.All()) +} + func compareRelationships(t testing.TB, expected, actual []artifact.Relationship) { assert.Equal(t, len(expected), len(actual), "number of relationships should match") for _, e := range expected { @@ -408,3 +443,94 @@ func TestReplace_NoExistingRelations(t *testing.T) { allRels := index.All() assert.Len(t, allRels, 0) } + +// Remove must scrub the removed node's edges from adjacent nodes too, otherwise query methods keep returning +// relationships that no longer exist. the original Remove only deleted the removed node's own maps, leaving +// dangling pointers in the other endpoints. +func TestRemove_NoStaleEdgesInAdjacentNodes(t *testing.T) { + p1 := pkg.Package{Name: "pkg-1", Version: "1"} + p2 := pkg.Package{Name: "pkg-2", Version: "2"} + p3 := pkg.Package{Name: "pkg-3", Version: "3"} + for _, p := range []*pkg.Package{&p1, &p2, &p3} { + p.SetID() + } + + r12 := artifact.Relationship{From: p1, To: p2, Type: artifact.DependencyOfRelationship} + r32 := artifact.Relationship{From: p3, To: p2, Type: artifact.DependencyOfRelationship} + index := NewIndex(r12, r32) + + index.Remove(p1.ID()) + + // p2 was on the To side of the removed edge; its incoming view must no longer include the stale p1 edge, + // but must still include the live p3 edge. + compareRelationships(t, []artifact.Relationship{r32}, index.To(p2, artifact.DependencyOfRelationship)) + assert.False(t, index.Contains(r12), "Contains must not report a removed edge") + assert.Len(t, index.References(p2), 1, "p2 should only reference the surviving p3 edge") + assert.True(t, index.Contains(r32), "surviving edge should remain") + compareRelationships(t, []artifact.Relationship{r32}, index.All()) +} + +// Replace must leave no dangling references to the old ID in adjacent nodes' query views. +func TestReplace_NoStaleEdgesAfterReplace(t *testing.T) { + main := pkg.Package{Name: "main-module"} // empty version + dep := pkg.Package{Name: "dep", Version: "1.0.0"} + main.SetID() + dep.SetID() + + oldEdge := artifact.Relationship{From: dep, To: main, Type: artifact.DependencyOfRelationship} + index := NewIndex(oldEdge) + + stubbed := main + stubbed.Version = "UNKNOWN" + stubbed.SetID() + require.NotEqual(t, main.ID(), stubbed.ID()) + + index.Replace(main.ID(), &stubbed) + + newEdge := artifact.Relationship{From: dep, To: stubbed, Type: artifact.DependencyOfRelationship} + // the dep's outgoing view must point at the stubbed node, not the old one + from := index.From(dep, artifact.DependencyOfRelationship) + require.Len(t, from, 1) + assert.Equal(t, stubbed.ID(), from[0].To.ID()) + assert.True(t, index.Contains(newEdge)) + assert.False(t, index.Contains(oldEdge), "stale edge to old ID must be gone") + assert.Empty(t, index.To(main), "old node should have no incoming edges") + compareRelationships(t, []artifact.Relationship{newEdge}, index.All()) +} + +// replacing a node with one that has the same ID must be a no-op, not silently wipe its edges. +func TestReplace_SameIDIsNoOp(t *testing.T) { + a := pkg.Package{Name: "a", Version: "1"} + b := pkg.Package{Name: "b", Version: "2"} + a.SetID() + b.SetID() + edge := artifact.Relationship{From: a, To: b, Type: artifact.DependencyOfRelationship} + index := NewIndex(edge) + + // same identity object -> same ID + index.Replace(b.ID(), b) + + compareRelationships(t, []artifact.Relationship{edge}, index.All()) + assert.True(t, index.Contains(edge)) +} + +// a self-edge on the replaced node should be remapped to replacement->replacement, not dropped. +func TestReplace_SelfEdge(t *testing.T) { + n := pkg.Package{Name: "n"} // empty version + n.SetID() + index := NewIndex(artifact.Relationship{From: n, To: n, Type: artifact.DependencyOfRelationship}) + + sn := n + sn.Version = "UNKNOWN" + sn.SetID() + require.NotEqual(t, n.ID(), sn.ID()) + + index.Replace(n.ID(), &sn) + + all := index.All() + require.Len(t, all, 1) + assert.Equal(t, sn.ID(), all[0].From.ID()) + assert.Equal(t, sn.ID(), all[0].To.ID()) + assert.Empty(t, index.From(n), "no edges should reference the old ID") + assert.Empty(t, index.To(n), "no edges should reference the old ID") +} From b71afc87fcdd01403e290384337beab873e0df87 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Fri, 19 Jun 2026 07:51:05 -0400 Subject: [PATCH 011/168] chore(deps): update tool versions (#4994) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .binny.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.binny.yaml b/.binny.yaml index 115e63729..92d156f88 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -24,7 +24,7 @@ tools: # used to parse JSON/YAML annotations on the fixture cache image - name: yq version: - want: v4.53.2 + want: v4.53.3 method: github-release with: repo: mikefarah/yq From 80d3b62de4d4923b11f4d200faee91447d1c94df Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 22 Jun 2026 09:47:58 -0400 Subject: [PATCH 012/168] bump go-make to v0.7.0 (#4999) Signed-off-by: Alex Goodman --- .make/go.mod | 6 +++--- .make/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.make/go.mod b/.make/go.mod index 3fb3f4147..1af7544c8 100644 --- a/.make/go.mod +++ b/.make/go.mod @@ -3,12 +3,12 @@ module github.com/anchore/syft/.make go 1.25.8 require ( - github.com/anchore/go-make v0.5.0 + github.com/anchore/go-make v0.7.0 github.com/goccy/go-yaml v1.19.2 ) require ( github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/sys v0.46.0 // indirect ) diff --git a/.make/go.sum b/.make/go.sum index 6ebe7cc64..2b15f37d8 100644 --- a/.make/go.sum +++ b/.make/go.sum @@ -1,10 +1,10 @@ -github.com/anchore/go-make v0.5.0 h1:VGlwqVhzowFb+9w/gaWUIid/YXvQZReBWKcj4LaZ3dM= -github.com/anchore/go-make v0.5.0/go.mod h1:Nc/tkwQHW1d1Vi8+0rtS/vSrH6pxieaUQXLdrctn+8g= +github.com/anchore/go-make v0.7.0 h1:qosSwNWV/SsLFc1pI0DlrCZ2BUSDcGDcSKM6HdlnT6c= +github.com/anchore/go-make v0.7.0/go.mod h1:4M6TnArb5w693VyWsgr5dCWrk2BLNu/ed4JUcsrzS34= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= From deb2fd92ef65b8b09d35a9f0c0d4aac4143218ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:12:11 +0000 Subject: [PATCH 013/168] chore(deps): bump github.com/containerd/containerd/v2 (#5001) Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.1...v2.3.2) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 98943414e..e339ffe80 100644 --- a/go.mod +++ b/go.mod @@ -170,7 +170,7 @@ require ( github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect github.com/containerd/containerd/api v1.11.1 // indirect - github.com/containerd/containerd/v2 v2.3.1 // indirect + github.com/containerd/containerd/v2 v2.3.2 // indirect github.com/containerd/continuity v0.5.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -306,12 +306,12 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/api v0.271.0 // indirect diff --git a/go.sum b/go.sum index 6ec8c27e3..21eec0037 100644 --- a/go.sum +++ b/go.sum @@ -296,8 +296,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.1 h1:4dVXBdlvotRBlaP2TmNbY/EGc06KJrMDDUqQdxX/HOk= -github.com/containerd/containerd/v2 v2.3.1/go.mod h1:xVoxGPWZBwwph8DF2IbDhriLKdHfjdpO0b3wFP9wQ1I= +github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM= +github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -1035,8 +1035,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1155,8 +1155,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1231,13 +1231,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1248,8 +1248,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 0e8d6deabeeaebba42fe129de4264b06fe16374f Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 22 Jun 2026 10:54:26 -0400 Subject: [PATCH 014/168] require tmpdir to exist for fingerprints (#5002) Signed-off-by: Alex Goodman --- Taskfile.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Taskfile.yaml b/Taskfile.yaml index 18cd532b8..d3d59426b 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -137,6 +137,7 @@ tasks: fingerprints: desc: Generate fingerprints for all non-docker test fixtures silent: true + deps: [tmpdir] # this will look for `testdata/Makefile` and invoke the `fingerprint` target to calculate all cache input fingerprint files generates: - '**/testdata/**/*.fingerprint' From 9c321691d483fb2cad06d0a99b6733559dcbb00c Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 22 Jun 2026 10:59:34 -0400 Subject: [PATCH 015/168] feat: SPDX 3 (#4269) Signed-off-by: Keith Zantow --- cmd/syft/internal/options/format_spdx_json.go | 10 +- .../internal/options/format_spdx_json_test.go | 6 +- go.mod | 6 +- go.sum | 12 +- .../common/spdxhelpers/to_syft_model.go | 12 +- .../common/spdxhelpers/to_syft_model_v3.go | 774 ++++++++++++++++++ .../spdxhelpers/to_syft_model_v3_test.go | 436 ++++++++++ syft/format/encoders_collection.go | 33 +- syft/format/encoders_collection_test.go | 75 ++ syft/format/internal/spdxutil/versions.go | 10 +- syft/format/spdxjson/decoder.go | 22 +- syft/format/spdxjson/encoder.go | 21 +- syft/format/spdxjson/encoder_test.go | 6 + syft/format/spdxjson/spdx3_decoder.go | 63 ++ syft/format/spdxjson/spdx3_decoder_test.go | 426 ++++++++++ .../spdxjson/testdata/identify/3.0.json | 141 ++++ test/cli/all_formats_convertible_test.go | 1 + test/cli/all_formats_expressible_test.go | 4 + test/cli/convert_cmd_test.go | 1 + test/cli/spdx_json_schema_test.go | 2 +- test/cli/spdx_tooling_validation_test.go | 85 +- .../testdata/image-java-spdx-tools/Dockerfile | 7 - .../testdata/image-java-spdx-tools/Makefile | 10 - 23 files changed, 2063 insertions(+), 100 deletions(-) create mode 100644 syft/format/common/spdxhelpers/to_syft_model_v3.go create mode 100644 syft/format/common/spdxhelpers/to_syft_model_v3_test.go create mode 100644 syft/format/spdxjson/spdx3_decoder.go create mode 100644 syft/format/spdxjson/spdx3_decoder_test.go create mode 100644 syft/format/spdxjson/testdata/identify/3.0.json delete mode 100644 test/cli/testdata/image-java-spdx-tools/Dockerfile delete mode 100644 test/cli/testdata/image-java-spdx-tools/Makefile diff --git a/cmd/syft/internal/options/format_spdx_json.go b/cmd/syft/internal/options/format_spdx_json.go index efdccb230..53e822650 100644 --- a/cmd/syft/internal/options/format_spdx_json.go +++ b/cmd/syft/internal/options/format_spdx_json.go @@ -13,12 +13,10 @@ func DefaultFormatSPDXJSON() FormatSPDXJSON { } func (o FormatSPDXJSON) config(v string) spdxjson.EncoderConfig { - var pretty bool + c := spdxjson.DefaultEncoderConfig() + c.Version = v if o.Pretty != nil { - pretty = *o.Pretty - } - return spdxjson.EncoderConfig{ - Version: v, - Pretty: pretty, + c.Pretty = *o.Pretty } + return c } diff --git a/cmd/syft/internal/options/format_spdx_json_test.go b/cmd/syft/internal/options/format_spdx_json_test.go index b81ef04c7..f7218d5f0 100644 --- a/cmd/syft/internal/options/format_spdx_json_test.go +++ b/cmd/syft/internal/options/format_spdx_json_test.go @@ -2,6 +2,8 @@ package options import ( "testing" + + "github.com/stretchr/testify/require" ) func TestFormatSPDXJSON_buildConfig(t *testing.T) { @@ -11,5 +13,7 @@ func TestFormatSPDXJSON_buildConfig(t *testing.T) { ft = setAllToNonZero(t, ft).(*FormatSPDXJSON) subject := ft.config("Version") - assertExpectedValue(t, subject) + require.Equal(t, "Version", subject.Version) + require.Equal(t, "2.3", subject.DefaultVersion) + require.True(t, subject.Pretty) } diff --git a/go.mod b/go.mod index e339ffe80..563220d46 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e github.com/sergi/go-diff v1.4.0 github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb - github.com/spdx/tools-golang v0.5.7 + github.com/spdx/tools-golang v0.6.0-rc4 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -127,7 +127,7 @@ require ( github.com/STARRY-S/zip v0.2.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/anchore/go-lzo v0.1.0 // indirect - github.com/anchore/go-struct-converter v0.1.0 // indirect + github.com/anchore/go-struct-converter v0.2.0-rc2 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect @@ -258,12 +258,14 @@ require ( github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/piprate/json-gold v0.7.0 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.7.0 // indirect github.com/pkg/xattr v0.4.12 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/go.sum b/go.sum index 21eec0037..031a56fc3 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,8 @@ github.com/anchore/go-macholibre v0.1.0 h1:qHbdusBZNcZM/uuKf1Psa9xxAFSoyRTps8GW9 github.com/anchore/go-macholibre v0.1.0/go.mod h1:eu0gbwaZ+ocVFJLePdmPPDKU8MboV1MKsUCr36Ckd5s= github.com/anchore/go-rpmdb v0.1.0 h1:Q8dc208/HYzCqhx0L1zurfm1UPil24hlo9NjkdFmLdE= github.com/anchore/go-rpmdb v0.1.0/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU= -github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= -github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4= +github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw= github.com/anchore/go-sync v0.1.0 h1:1TEZM7jISrvtoBMOF79xP0caQKASAtgW1yKqc0EjyZg= github.com/anchore/go-sync v0.1.0/go.mod h1:Iposeub0kHipoTei1icj4Tys0SJN+cCdxEnkS7bZUFs= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= @@ -789,6 +789,8 @@ github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7ol github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/piprate/json-gold v0.7.0 h1:bEMirgA5y8Z2loTQfxyIFfY+EflxH1CTP6r/KIlcJNw= +github.com/piprate/json-gold v0.7.0/go.mod h1:RVhE35veDX19r5gfUAR+IYHkAUuPwJO8Ie/qVeFaIzw= github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -809,6 +811,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -877,8 +881,8 @@ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cw github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb h1:bLo8hvc8XFm9J47r690TUKBzcjSWdJDxmjXJZ+/f92U= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= -github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= +github.com/spdx/tools-golang v0.6.0-rc4 h1:2GkvNr0DcnJHY9BDm3OYHo229jZS/h4qYDK+tHYXPOo= +github.com/spdx/tools-golang v0.6.0-rc4/go.mod h1:ruCHu3shgy7bVbZ7gtEU4Gq4fI08n2SdXtgV5PoN/OM= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= diff --git a/syft/format/common/spdxhelpers/to_syft_model.go b/syft/format/common/spdxhelpers/to_syft_model.go index 80bbbf964..c375114bb 100644 --- a/syft/format/common/spdxhelpers/to_syft_model.go +++ b/syft/format/common/spdxhelpers/to_syft_model.go @@ -399,7 +399,7 @@ func collectDocRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out to = toLocation case helpers.OtherRelationship: // Encoding uses a specifically formatted comment... - if strings.Index(r.RelationshipComment, string(artifact.EvidentByRelationship)) == 0 { + if strings.HasPrefix(r.RelationshipComment, string(artifact.EvidentByRelationship)) { typ = artifact.EvidentByRelationship to = toLocation } @@ -418,7 +418,7 @@ func collectDocRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out to = toPackage case helpers.OtherRelationship: // Encoding uses a specifically formatted comment... - if strings.Index(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) == 0 { + if strings.HasPrefix(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) { typ = artifact.OwnershipByFileOverlapRelationship to = toPackage } @@ -462,10 +462,10 @@ func collectPackageFileRelationships(spdxIDMap map[string]any, doc *spdx.Documen func toSyftCoordinates(f *spdx.File) file.Coordinates { const layerIDPrefix = "layerID: " var fileSystemID string - if strings.Index(f.FileComment, layerIDPrefix) == 0 { + if strings.HasPrefix(f.FileComment, layerIDPrefix) { fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix) } - if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 { + if strings.HasPrefix(string(f.FileSPDXIdentifier), layerIDPrefix) { fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix) } return file.Coordinates{ @@ -481,8 +481,8 @@ func toSyftLocation(f *spdx.File) file.Location { func requireAndTrimPrefix(val any, prefix string) string { if v, ok := val.(string); ok { - if i := strings.Index(v, prefix); i == 0 { - return strings.Replace(v, prefix, "", 1) + if strings.HasPrefix(v, prefix) { + return strings.TrimPrefix(v, prefix) } } return "" diff --git a/syft/format/common/spdxhelpers/to_syft_model_v3.go b/syft/format/common/spdxhelpers/to_syft_model_v3.go new file mode 100644 index 000000000..336c2d30b --- /dev/null +++ b/syft/format/common/spdxhelpers/to_syft_model_v3.go @@ -0,0 +1,774 @@ +package spdxhelpers + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + + spdx "github.com/spdx/tools-golang/spdx/v3/v3_0" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/format/internal" + "github.com/anchore/syft/syft/format/internal/spdxutil/helpers" + "github.com/anchore/syft/syft/license" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +func ToSyftModelV3(doc *spdx.Document) (*sbom.SBOM, error) { + if doc == nil { + return nil, errors.New("cannot convert SPDX document to Syft model because document is nil") + } + + spdxMap := ptrMap[any]{} + + s := &sbom.SBOM{ + Source: v3extractSource(spdxMap, doc), + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(), + FileMetadata: map[file.Coordinates]file.Metadata{}, + FileDigests: map[file.Coordinates][]file.Digest{}, + LinuxDistribution: v3findLinuxReleaseByPURL(doc), + }, + } + + relationships := v3relationshipMap(doc) + + v3collectSyftPackages(s, spdxMap, relationships, doc) + + v3collectSyftFiles(s, spdxMap, doc) + + s.Relationships = v3toSyftRelationships(spdxMap, doc) + + return s, nil +} + +func v3removePackage(packages spdx.ElementList, remove spdx.AnyPackage) (pkgs spdx.ElementList) { + for _, p := range packages { + if p == remove { + continue + } + pkgs = append(pkgs, p) + } + return pkgs +} + +func v3removeRelationships(elements spdx.ElementList, element spdx.AnyElement) (relations spdx.ElementList) { + for _, e := range elements { + if r, ok := e.(spdx.AnyRelationship); ok { + if r != nil && r.GetFrom() == element { + continue + } + + if r != nil { + var tos spdx.ElementList + for _, to := range r.GetTo() { + if to == element { + continue + } + tos = append(tos, to) + } + r.SetTo(tos) + if len(r.GetTo()) == 0 { + continue + } + } + + relations = append(relations, r) + } else { + relations = append(relations, e) + } + } + return relations +} + +func v3findRootPackages(doc spdx.AnyElementCollection) (out spdx.PackageList) { + for _, p := range doc.GetRootElements().Packages() { + out = append(out, p) + } + for _, s := range doc.GetRootElements().SBOMs() { + for _, p := range s.GetRootElements().Packages() { + out = append(out, p) + } + } + return +} + +func v3extractSource(spdxMap ptrMap[any], doc *spdx.Document) source.Description { + namespace := doc.ID + if namespace == "" && len(doc.NamespaceMaps) > 0 { + namespace = string(doc.NamespaceMaps[0].GetNamespace()) + } + src := extractSourceFromNamespace(namespace) + + rootPackages := v3findRootPackages(doc) + + if len(rootPackages) != 1 { + return src + } + + p := rootPackages[0] + + switch p.GetPrimaryPurpose() { + case spdx.SoftwarePurpose_Container: + src = v3containerSource(p) + case spdx.SoftwarePurpose_File: + src = v3fileSource(p) + default: + return src + } + + spdxMap.Set(p, src) + + doc.Elements = v3removePackage(doc.Elements, p) + doc.Elements = v3removeRelationships(doc.Elements, p) + + return src +} + +func v3containerSource(p spdx.AnyPackage) source.Description { + container := p.GetName() + v := p.GetVersion() + if v != "" { + container += ":" + v + } + + digest := "" + if len(p.GetVerifiedUsing().Hashes()) > 0 { + h := p.GetVerifiedUsing().Hashes()[0] + if h != nil { + digest = fmt.Sprintf("%s:%s", v3fromChecksumAlgorithm(h.GetAlgorithm()), h.GetValue()) + } + } + + supplier := "" + if p.GetSuppliedBy() != nil { + supplier = v3agentString(p.GetSuppliedBy()) + } + + return source.Description{ + ID: p.GetID(), + Name: p.GetName(), + Version: p.GetVersion(), + Supplier: supplier, + Metadata: source.ImageMetadata{ + UserInput: container, + ID: p.GetID(), + Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool + ManifestDigest: digest, + }, + } +} + +func v3fileSource(p spdx.AnyPackage) source.Description { + typeRegex := regexp.MustCompile("DocumentRoot-([^-]+)-.*$") + typeName := typeRegex.ReplaceAllString(p.GetID(), "$1") + + var version string + var metadata any + switch { + case typeName == prefixDirectory: + // is a Syft SBOM, explicitly a directory source + metadata, version = v3directorySourceMetadata(p) + case typeName == prefixFile: + // is a Syft SBOM, explicitly a file source + metadata, version = v3fileSourceMetadata(p) + case isDirectory(p.GetName()): + // is a non-Syft SBOM, which looks like a directory + metadata, version = v3directorySourceMetadata(p) + default: + // is a non-Syft SBOM, which is probably a file + metadata, version = v3fileSourceMetadata(p) + } + + supplier := "" + if p.GetSuppliedBy() != nil { + supplier = p.GetSuppliedBy().GetName() + } + + return source.Description{ + ID: p.GetID(), + Name: p.GetName(), + Version: version, + Supplier: supplier, + Metadata: metadata, + } +} + +func v3fileSourceMetadata(p spdx.AnyPackage) (any, string) { + version := p.GetVersion() + + m := source.FileMetadata{ + Path: p.GetName(), + } + // if this is a Syft SBOM, we might have output a digest as the version + checksum := v3toChecksum(p.GetVersion()) + for _, d := range p.GetVerifiedUsing().Hashes() { + if checksum != nil && checksum.GetValue() == d.GetValue() { + version = "" + } + m.Digests = append(m.Digests, file.Digest{ + Algorithm: v3fromChecksumAlgorithm(d.GetAlgorithm()), + Value: d.GetValue(), + }) + } + + return m, version +} + +func v3directorySourceMetadata(p spdx.AnyPackage) (any, string) { + return source.DirectoryMetadata{ + Path: p.GetName(), + Base: "", + }, p.GetVersion() +} + +func v3findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release { + for _, p := range doc.Elements.Packages() { + purlValue := v3findPURLValue(p) + if purlValue == "" { + continue + } + purl, err := packageurl.FromString(purlValue) + if err != nil { + log.Warnf("unable to parse purl: %s", purlValue) + continue + } + distro := v3findQualifierValue(purl, pkg.PURLQualifierDistro) + if distro != "" { + parts := strings.Split(distro, "-") + name := parts[0] + version := "" + if len(parts) > 1 { + version = parts[1] + } + return &linux.Release{ + PrettyName: name, + Name: name, + ID: name, + IDLike: []string{name}, + Version: version, + VersionID: version, + } + } + } + + return nil +} + +func v3collectSyftPackages(s *sbom.SBOM, spdxMap ptrMap[any], relationships ptrMap[[]spdx.AnyRelationship], doc *spdx.Document) { + skipIDs := v3packageIDsToSkip(doc) + found := ptrMap[struct{}]{} + // tools-golang collects all elements from the JSON LD @graph to the root SpdxDocument.Elements property + for _, elementList := range []spdx.ElementList{doc.Elements, doc.RootElements} { + for _, p := range elementList.Packages() { + if p == nil || skipIDs.Has(p) || found.Has(p) { + continue + } + found.Set(p, struct{}{}) + syftPkg := v3toSyftPackage(relationships, p) + spdxMap.Set(p, syftPkg) + s.Artifacts.Packages.Add(syftPkg) + } + } +} + +func v3collectSyftFiles(s *sbom.SBOM, spdxMap ptrMap[any], doc *spdx.Document) { + found := ptrMap[struct{}]{} + for _, elementList := range []spdx.ElementList{doc.Elements, doc.RootElements} { + for _, f := range elementList.Files() { + if found.Has(f) { + continue + } + found.Set(f, struct{}{}) + l := v3toSyftLocation(f) + spdxMap.Set(f, l) + + s.Artifacts.FileMetadata[l.Coordinates] = v3toFileMetadata(f) + s.Artifacts.FileDigests[l.Coordinates] = v3toFileDigests(f) + } + } +} + +func v3toFileDigests(f spdx.AnyFile) (digests []file.Digest) { + for _, h := range f.GetVerifiedUsing().Hashes() { + digests = append(digests, file.Digest{ + Algorithm: v3fromChecksumAlgorithm(h.GetAlgorithm()), + Value: h.GetValue(), + }) + } + return digests +} + +func v3fromChecksumAlgorithm(algorithm spdx.HashAlgorithm) string { + // it might be better to have a specific case statement with constants + parts := strings.Split(algorithm.GetID(), "/") + return strings.ToLower(parts[len(parts)-1]) +} + +func v3toFileMetadata(f spdx.AnyFile) (meta file.Metadata) { + // FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes + if f.GetContentType() != "" { + meta.MIMEType = f.GetContentType() + } + return meta +} + +func v3toSyftRelationships(spdxMap ptrMap[any], doc *spdx.Document) []artifact.Relationship { + out := v3collectDocRelationships(spdxMap, doc) + + return out +} + +//nolint:gocognit +func v3collectDocRelationships(spdxMap ptrMap[any], doc *spdx.Document) (out []artifact.Relationship) { + for _, r := range doc.Elements.Relationships() { + from := r.GetFrom() + if from == nil || from.GetID() == "" { + log.Debugf("ignoring relationship to external document: %+v", r) + continue + } + a := spdxMap.Get(from) + + for _, to := range r.GetTo() { + b := spdxMap.Get(to) + from, fromOk := a.(pkg.Package) + toPackage, toPackageOk := b.(pkg.Package) + toLocation, toLocationOk := b.(file.Location) + //nolint:staticcheck + if !fromOk || !(toPackageOk || toLocationOk) { + log.Debugf("unable to find valid relationship mapping from SPDX, ignoring: (from: %+v) (to: %+v)", a, b) + continue + } + var to artifact.Identifiable + var typ artifact.RelationshipType + if toLocationOk { + switch r.GetType() { + case spdx.RelationshipType_Contains: + typ = artifact.ContainsRelationship + to = toLocation + case spdx.RelationshipType_Other: + // Encoding uses a specifically formatted comment... + if strings.HasPrefix(r.GetComment(), string(artifact.EvidentByRelationship)) { + typ = artifact.EvidentByRelationship + to = toLocation + } + } + } else { + switch r.GetType() { + case spdx.RelationshipType_DependsOn: + typ = artifact.DependencyOfRelationship + to = from + from = toPackage + case spdx.RelationshipType_Contains: + typ = artifact.ContainsRelationship + to = toPackage + case spdx.RelationshipType_Other: + // Encoding uses a specifically formatted comment... + if strings.HasPrefix(r.GetComment(), string(artifact.OwnershipByFileOverlapRelationship)) { + typ = artifact.OwnershipByFileOverlapRelationship + to = toPackage + } + } + } + if typ != "" && to != nil { + out = append(out, artifact.Relationship{ + From: from, + To: to, + Type: typ, + }) + } + } + } + return out +} + +func v3toSyftCoordinates(f spdx.AnyFile) file.Coordinates { + const layerIDPrefix = "layerID: " + var fileSystemID string + if strings.HasPrefix(f.GetComment(), layerIDPrefix) { + fileSystemID = strings.TrimPrefix(f.GetComment(), layerIDPrefix) + } + if strings.HasPrefix(f.GetID(), layerIDPrefix) { + fileSystemID = strings.TrimPrefix(f.GetID(), layerIDPrefix) + } + return file.Coordinates{ + RealPath: f.GetName(), + FileSystemID: fileSystemID, + } +} + +func v3toSyftLocation(f spdx.AnyFile) file.Location { + l := file.NewVirtualLocationFromCoordinates(v3toSyftCoordinates(f), f.GetName()) + return l +} + +func v3findQualifierValue(purl packageurl.PackageURL, qualifier string) string { + for _, q := range purl.Qualifiers { + if q.Key == qualifier { + return q.Value + } + } + return "" +} + +func v3extractPkgInfo(p spdx.AnyPackage) pkgInfo { + pu := v3findPURLValue(p) + purl, err := packageurl.FromString(pu) + if err != nil { + return pkgInfo{} + } + return pkgInfo{ + purl, + pkg.TypeByName(purl.Type), + pkg.LanguageByName(purl.Type), + } +} + +func v3toSyftPackage(relationships ptrMap[[]spdx.AnyRelationship], p spdx.AnyPackage) pkg.Package { + info := v3extractPkgInfo(p) + sP := &pkg.Package{ + Type: info.typ, + Name: p.GetName(), + Version: p.GetVersion(), + Licenses: pkg.NewLicenseSet(v3parseSPDXLicenses(relationships, p)...), + CPEs: v3extractCPEs(p), + PURL: v3purlValue(info.purl), + Language: info.lang, + Metadata: v3extractMetadata(p, info), + } + + internal.Backfill(sP) + + if p.GetID() != "" { + // always prefer the IDs from the SBOM over derived IDs + sP.OverrideID(artifact.ID(p.GetID())) + } else { + sP.SetID() + } + + return *sP +} + +func v3purlValue(purl packageurl.PackageURL) string { + val := purl.String() + if _, err := packageurl.FromString(val); err != nil { + return "" + } + return val +} + +func v3parseSPDXLicenses(relationships ptrMap[[]spdx.AnyRelationship], p spdx.AnyPackage) []pkg.License { + licenses := make([]pkg.License, 0) + + // licenses are defined with relationships in SPDX 3, see: + // https://github.com/spdx/tools-golang/blob/spdx3/spdx/v3/v3_0/convert.go#L536 + rels := relationships.Get(p) + for _, r := range rels { + if r.GetType() == spdx.RelationshipType_HasConcludedLicense { + licenses = append(licenses, v3toSyftLicenses(license.Concluded, r.GetTo().LicenseInfos()...)...) + } + if r.GetType() == spdx.RelationshipType_HasDeclaredLicense { + licenses = append(licenses, v3toSyftLicenses(license.Declared, r.GetTo().LicenseInfos()...)...) + } + } + + return licenses +} + +func v3toSyftLicenses(licenseType license.Type, licenses ...spdx.AnyLicenseInfo) []pkg.License { + var out []pkg.License + for _, lic := range licenses { + if lic == nil { + continue + } + value := v3licenseInfoToExpression(lic) + if value == "" { + log.Debugf("skipping SPDX license during import: %#v", lic) + continue + } + l := pkg.NewLicense(value) + if l.Value != "" { + l.Type = licenseType + out = append(out, l) + } + } + return out +} + +// v3licenseInfoToExpression recursively converts an SPDX 3.0 license info object to an SPDX expression string. +func v3licenseInfoToExpression(info spdx.AnyLicenseInfo) string { + switch li := info.(type) { + case spdx.AnyLicenseExpression: + return li.GetLicenseExpression() + case spdx.AnyOrLaterOperator: + subject := v3licenseInfoToExpression(li.GetSubjectLicense()) + if subject != "" && !strings.HasSuffix(subject, "+") { + return subject + "+" + } + case spdx.AnyWithAdditionOperator: + subject := v3licenseInfoToExpression(li.GetSubjectExtendableLicense()) + addition := li.GetSubjectAddition() + if subject != "" && addition != nil { + return subject + " WITH " + addition.GetAdditionText() + } + case spdx.AnyConjunctiveLicenseSet: + var parts []string + for _, m := range li.GetMembers() { + if e := v3licenseInfoToExpression(m); e != "" { + parts = append(parts, e) + } + } + if len(parts) > 0 { + return strings.Join(parts, " AND ") + } + case spdx.AnyDisjunctiveLicenseSet: + var parts []string + for _, m := range li.GetMembers() { + if e := v3licenseInfoToExpression(m); e != "" { + parts = append(parts, e) + } + } + if len(parts) > 0 { + return "(" + strings.Join(parts, " OR ") + ")" + } + case spdx.AnyListedLicense: + return li.GetName() + case spdx.AnyCustomLicense: + if li.GetID() != "" { + return li.GetID() + } + return li.GetName() + } + return "" +} + +//nolint:funlen +func v3extractMetadata(p spdx.AnyPackage, info pkgInfo) any { + arch := info.qualifierValue(pkg.PURLQualifierArch) + upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream) + upstream := strings.SplitN(upstreamValue, "@", 2) + upstreamName := upstream[0] + upstreamVersion := "" + if len(upstream) > 1 { + upstreamVersion = upstream[1] + } + supplier := "" + if p.GetSuppliedBy() != nil { + supplier = v3agentString(p.GetSuppliedBy()) + } + originator := "" + if len(p.GetOriginatedBy()) > 0 { + // FIXME there could be multiple + originator = v3agentString(p.GetOriginatedBy()[0]) + } + switch info.typ { + case pkg.ApkPkg: + return pkg.ApkDBEntry{ + Package: p.GetName(), + OriginPackage: upstreamName, + Maintainer: supplier, + Version: p.GetVersion(), + Architecture: arch, + URL: string(p.GetHomePage()), + Description: p.GetDescription(), + } + case pkg.RpmPkg: + converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch)) + var epoch *int + if err != nil { + epoch = nil + } else { + epoch = &converted + } + return pkg.RpmDBEntry{ + Name: p.GetName(), + Version: p.GetVersion(), + Epoch: epoch, + Arch: arch, + SourceRpm: upstreamValue, + Vendor: originator, + } + case pkg.DebPkg: + return pkg.DpkgDBEntry{ + Package: p.GetName(), + Source: upstreamName, + Version: p.GetVersion(), + SourceVersion: upstreamVersion, + Architecture: arch, + Maintainer: originator, + } + case pkg.JavaPkg: + var digests []file.Digest + for _, value := range p.GetVerifiedUsing() { + h, _ := value.(spdx.AnyHash) + if h != nil { + digests = append(digests, file.Digest{Algorithm: v3fromChecksumAlgorithm(h.GetAlgorithm()), Value: h.GetValue()}) + } + } + return pkg.JavaArchive{ + ArchiveDigests: digests, + } + case pkg.GoModulePkg: + var h1Digest string + for _, value := range p.GetVerifiedUsing() { + h, _ := value.(spdx.AnyHash) + if h == nil { + continue + } + digest, err := helpers.HDigestFromSHA(v3fromChecksumAlgorithm(h.GetAlgorithm()), h.GetValue()) + if err != nil { + log.Debugf("invalid h1digest: %v %v", value, err) + continue + } + h1Digest = digest + break + } + return pkg.GolangBinaryBuildinfoEntry{ + H1Digest: h1Digest, + } + } + return nil +} + +func v3agentString(agent spdx.AnyAgent) string { + out := "" + switch o := agent.(type) { + case spdx.AnyOrganization: + out = o.GetName() + case spdx.AnyPerson: + out = o.GetName() + } + if out == helpers.NOASSERTION { + return "" + } + return out +} + +func v3findPURLValue(p spdx.AnyPackage) string { + if p.GetPackageURL() != "" { + return string(p.GetPackageURL()) + } + for _, r := range p.GetExternalIdentifiers() { + if r.GetType() == spdx.ExternalIdentifierType_PackageURL { + for _, l := range r.GetIdentifierLocators() { + // FIXME multiple values + return string(l) + } + } + } + return "" +} + +func v3extractCPEs(p spdx.AnyPackage) (cpes []cpe.CPE) { + for _, r := range p.GetExternalIdentifiers() { + if r.GetType() == spdx.ExternalIdentifierType_Cpe23 || r.GetType() == spdx.ExternalIdentifierType_Cpe22 { + c, err := cpe.New(r.GetIdentifier(), cpe.DeclaredSource) + if err == nil { + cpes = append(cpes, c) + continue + } + log.Tracef("unable to extract SPDX IDENTIFIER CPE=%q: %+v", r.GetIdentifier(), err) + // try the locators + for _, l := range r.GetIdentifierLocators() { + c, err = cpe.New(string(l), cpe.DeclaredSource) + if err != nil { + log.Tracef("unable to extract SPDX CPE=%q: %+v", l, err) + continue + } + cpes = append(cpes, c) + } + } + } + return cpes +} + +// v3packageIDsToSkip returns a set of packageIDs that should not be imported +func v3packageIDsToSkip(doc *spdx.Document) ptrMap[struct{}] { + skipIDs := ptrMap[struct{}]{} + for _, r := range doc.Elements.Relationships() { + if r != nil && r.GetFrom() != nil && r.GetType() == spdx.RelationshipType_Generates { + skipIDs.Set(r.GetFrom(), struct{}{}) // flipped from GENERATED_FROM + } + } + return skipIDs +} + +// toChecksum takes a checksum in the format : and returns an spdx.Checksum or nil if the string is invalid +func v3toChecksum(algorithmHash string) spdx.AnyHash { + parts := strings.Split(algorithmHash, ":") + if len(parts) < 2 { + return nil + } + return &spdx.Hash{ + Algorithm: v3toChecksumAlgorithm(parts[0]), + Value: parts[1], + } +} + +func v3toChecksumAlgorithm(algorithm string) spdx.HashAlgorithm { + // this needs to be an uppercase version of our algorithm + switch strings.ToLower(algorithm) { + case "sha1": + return spdx.HashAlgorithm_Sha1 + case "sha256": + return spdx.HashAlgorithm_Sha256 + case "sha384": + return spdx.HashAlgorithm_Sha384 + case "sha512": + return spdx.HashAlgorithm_Sha512 + case "md5": + return spdx.HashAlgorithm_Md5 + } + return spdx.HashAlgorithm{} +} + +func v3relationshipMap(doc *spdx.Document) ptrMap[[]spdx.AnyRelationship] { + relationships := ptrMap[[]spdx.AnyRelationship]{} + for _, r := range doc.Elements.Relationships() { + existing := relationships.Get(r.GetFrom()) + relationships.Set(r.GetFrom(), append(existing, r)) + } + return relationships +} + +// SPDX 3 values are stored as pointers, and there is a distinct possibility that an ID +// will be blank if it was a JSON-LD blank node in the document, as these IDs are not persisted +// nor should they be persisted from decoding + +type ptrMap[T any] map[reflect.Value]T + +func (s ptrMap[T]) Set(k any, v T) { + s[ptrTo(k)] = v +} + +func (s ptrMap[T]) Get(k any) T { + return s[ptrTo(k)] +} + +func (s ptrMap[T]) Remove(k any) { + delete(s, ptrTo(k)) +} + +func (s ptrMap[T]) Has(k any) bool { + _, ok := s[ptrTo(k)] + return ok +} + +func ptrTo(k any) reflect.Value { + rv := reflect.ValueOf(k) + if rv.Kind() != reflect.Pointer { + // this case is a programming problem -- all objects in the SPDX 3 model are pointers and may not have IDs set + panic(fmt.Errorf("value is not a pointer; comparable SPDX 3 elements are pointers, this is probably an implementation issue: %#v", k)) + } + return rv +} diff --git a/syft/format/common/spdxhelpers/to_syft_model_v3_test.go b/syft/format/common/spdxhelpers/to_syft_model_v3_test.go new file mode 100644 index 000000000..f00399b21 --- /dev/null +++ b/syft/format/common/spdxhelpers/to_syft_model_v3_test.go @@ -0,0 +1,436 @@ +package spdxhelpers + +import ( + "testing" + + spdx "github.com/spdx/tools-golang/spdx/v3/v3_0" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/license" + "github.com/anchore/syft/syft/pkg" +) + +func TestToSyftModelV3_documentConversion(t *testing.T) { + // Build SPDX 3.0 elements + pkg1 := &spdx.Package{ + ID: "pkg-1-id", + Name: "pkg-1", + Version: "1.0.0", + ExternalIdentifiers: spdx.ExternalIdentifierList{ + &spdx.ExternalIdentifier{ + Type: spdx.ExternalIdentifierType_PackageURL, + Identifier: "pkg:npm/pkg-1@1.0.0", + IdentifierLocators: []spdx.URI{"pkg:npm/pkg-1@1.0.0"}, + }, + }, + } + pkg2 := &spdx.Package{ + ID: "pkg-2-id", + Name: "pkg-2", + Version: "2.0.0", + ExternalIdentifiers: spdx.ExternalIdentifierList{ + &spdx.ExternalIdentifier{ + Type: spdx.ExternalIdentifierType_PackageURL, + Identifier: "pkg:npm/pkg-2@2.0.0", + IdentifierLocators: []spdx.URI{"pkg:npm/pkg-2@2.0.0"}, + }, + }, + } + + file1 := &spdx.File{ + ID: "file-1-id", + Name: "/src/main.go", + VerifiedUsing: spdx.IntegrityMethodList{ + &spdx.Hash{ + Algorithm: spdx.HashAlgorithm_Sha256, + Value: "abc123", + }, + }, + } + file2 := &spdx.File{ + ID: "file-2-id", + Name: "/src/util.go", + } + + // licenses + pkg1ConcludedLicense := &spdx.ListedLicense{Name: "MIT"} + pkg1DeclaredLicense := &spdx.DisjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "MIT"}, + &spdx.ListedLicense{Name: "Apache-2.0"}, + }, + } + pkg2ConcludedLicense := &spdx.ConjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "BSD-3-Clause"}, + &spdx.OrLaterOperator{ + SubjectLicense: &spdx.ListedLicense{Name: "GPL-2.0"}, + }, + }, + } + + // relationships + pkg1DependsOnPkg2 := &spdx.Relationship{ + From: pkg1, + To: spdx.ElementList{pkg2}, + Type: spdx.RelationshipType_DependsOn, + } + pkg1ContainsFile1 := &spdx.Relationship{ + From: pkg1, + To: spdx.ElementList{file1}, + Type: spdx.RelationshipType_Contains, + } + pkg2ContainsFile2 := &spdx.Relationship{ + From: pkg2, + To: spdx.ElementList{file2}, + Type: spdx.RelationshipType_Contains, + } + pkg1HasConcludedLicense := &spdx.Relationship{ + From: pkg1, + To: spdx.ElementList{pkg1ConcludedLicense}, + Type: spdx.RelationshipType_HasConcludedLicense, + } + pkg1HasDeclaredLicense := &spdx.Relationship{ + From: pkg1, + To: spdx.ElementList{pkg1DeclaredLicense}, + Type: spdx.RelationshipType_HasDeclaredLicense, + } + pkg2HasConcludedLicense := &spdx.Relationship{ + From: pkg2, + To: spdx.ElementList{pkg2ConcludedLicense}, + Type: spdx.RelationshipType_HasConcludedLicense, + } + + sbomElement := &spdx.SBOM{ + RootElements: spdx.ElementList{pkg1}, + Elements: spdx.ElementList{ + pkg1, pkg2, + file1, file2, + pkg1DependsOnPkg2, + pkg1ContainsFile1, + pkg2ContainsFile2, + pkg1HasConcludedLicense, + pkg1HasDeclaredLicense, + pkg2HasConcludedLicense, + }, + } + + doc := &spdx.Document{ + SpdxDocument: spdx.SpdxDocument{ + ID: "https://example.org/test-doc", + Name: "test-document", + RootElements: spdx.ElementList{sbomElement}, + Elements: spdx.ElementList{ + sbomElement, + pkg1, pkg2, + file1, file2, + pkg1DependsOnPkg2, + pkg1ContainsFile1, + pkg2ContainsFile2, + pkg1HasConcludedLicense, + pkg1HasDeclaredLicense, + pkg2HasConcludedLicense, + }, + }, + } + + // Convert + result, err := ToSyftModelV3(doc) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify packages + allPkgs := result.Artifacts.Packages.Sorted() + require.Len(t, allPkgs, 2, "expected 2 packages") + + pkgsByName := map[string]pkg.Package{} + for _, p := range allPkgs { + pkgsByName[p.Name] = p + } + + syftPkg1, ok := pkgsByName["pkg-1"] + require.True(t, ok, "pkg-1 not found") + assert.Equal(t, "1.0.0", syftPkg1.Version) + assert.Equal(t, "pkg:npm/pkg-1@1.0.0", syftPkg1.PURL) + + syftPkg2, ok := pkgsByName["pkg-2"] + require.True(t, ok, "pkg-2 not found") + assert.Equal(t, "2.0.0", syftPkg2.Version) + assert.Equal(t, "pkg:npm/pkg-2@2.0.0", syftPkg2.PURL) + + // Verify pkg-1 licenses: concluded MIT + declared (MIT OR Apache-2.0) + pkg1Licenses := syftPkg1.Licenses.ToSlice() + require.NotEmpty(t, pkg1Licenses, "pkg-1 should have licenses") + + var pkg1Concluded, pkg1Declared []pkg.License + for _, l := range pkg1Licenses { + switch l.Type { + case license.Concluded: + pkg1Concluded = append(pkg1Concluded, l) + case license.Declared: + pkg1Declared = append(pkg1Declared, l) + } + } + require.Len(t, pkg1Concluded, 1) + assert.Equal(t, "MIT", pkg1Concluded[0].Value) + require.Len(t, pkg1Declared, 1) + assert.Equal(t, "(MIT OR Apache-2.0)", pkg1Declared[0].Value) + + // Verify pkg-2 licenses: concluded BSD-3-Clause AND GPL-2.0-only+ + pkg2Licenses := syftPkg2.Licenses.ToSlice() + require.NotEmpty(t, pkg2Licenses, "pkg-2 should have licenses") + + var pkg2Concluded []pkg.License + for _, l := range pkg2Licenses { + if l.Type == license.Concluded { + pkg2Concluded = append(pkg2Concluded, l) + } + } + require.Len(t, pkg2Concluded, 1) + assert.Equal(t, "BSD-3-Clause AND GPL-2.0+", pkg2Concluded[0].Value) + + // Verify files + coords1 := file.Coordinates{RealPath: "/src/main.go"} + digests, ok := result.Artifacts.FileDigests[coords1] + require.True(t, ok, "file1 digests not found") + require.Len(t, digests, 1) + assert.Equal(t, "sha256", digests[0].Algorithm) + assert.Equal(t, "abc123", digests[0].Value) + + coords2 := file.Coordinates{RealPath: "/src/util.go"} + _, ok = result.Artifacts.FileMetadata[coords2] + assert.True(t, ok, "file2 metadata not found") + + // Verify relationships + require.NotEmpty(t, result.Relationships) + + foundDependsOn := false + foundPkg1ContainsFile := false + foundPkg2ContainsFile := false + for _, rel := range result.Relationships { + fromPkg, fromOk := rel.From.(pkg.Package) + if !fromOk { + continue + } + switch toPkg := rel.To.(type) { + case pkg.Package: + if fromPkg.Name == "pkg-2" && toPkg.Name == "pkg-1" && rel.Type == artifact.DependencyOfRelationship { + foundDependsOn = true + } + case file.Location: + if fromPkg.Name == "pkg-1" && toPkg.RealPath == "/src/main.go" && rel.Type == artifact.ContainsRelationship { + foundPkg1ContainsFile = true + } + if fromPkg.Name == "pkg-2" && toPkg.RealPath == "/src/util.go" && rel.Type == artifact.ContainsRelationship { + foundPkg2ContainsFile = true + } + } + } + assert.True(t, foundDependsOn, "expected pkg-2 DependencyOf pkg-1 relationship") + assert.True(t, foundPkg1ContainsFile, "expected pkg-1 Contains /src/main.go relationship") + assert.True(t, foundPkg2ContainsFile, "expected pkg-2 Contains /src/util.go relationship") +} + +func Test_v3licenseInfoToExpression(t *testing.T) { + tests := []struct { + name string + info spdx.AnyLicenseInfo + expected string + }{ + { + name: "listed license", + info: &spdx.ListedLicense{Name: "MIT"}, + expected: "MIT", + }, + { + name: "custom license with ID", + info: &spdx.CustomLicense{ID: "LicenseRef-Custom-1", Name: "Custom License"}, + expected: "LicenseRef-Custom-1", + }, + { + name: "custom license without ID falls back to name", + info: &spdx.CustomLicense{Name: "Custom License"}, + expected: "Custom License", + }, + { + name: "license expression", + info: &spdx.LicenseExpression{LicenseExpression: "MIT AND Apache-2.0"}, + expected: "MIT AND Apache-2.0", + }, + { + name: "or-later operator", + info: &spdx.OrLaterOperator{ + SubjectLicense: &spdx.ListedLicense{Name: "GPL-2.0"}, + }, + expected: "GPL-2.0+", + }, + { + name: "conjunctive license set (AND)", + info: &spdx.ConjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "MIT"}, + &spdx.ListedLicense{Name: "Apache-2.0"}, + }, + }, + expected: "MIT AND Apache-2.0", + }, + { + name: "disjunctive license set (OR)", + info: &spdx.DisjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "MIT"}, + &spdx.ListedLicense{Name: "Apache-2.0"}, + }, + }, + expected: "(MIT OR Apache-2.0)", + }, + { + name: "with-addition operator", + info: &spdx.WithAdditionOperator{ + SubjectExtendableLicense: &spdx.ListedLicense{Name: "GPL-2.0-only"}, + SubjectAddition: &spdx.ListedLicenseException{AdditionText: "Classpath-exception-2.0"}, + }, + expected: "GPL-2.0-only WITH Classpath-exception-2.0", + }, + { + name: "nested: (MIT OR Apache-2.0) AND GPL-2.0", + info: &spdx.ConjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.DisjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "MIT"}, + &spdx.ListedLicense{Name: "Apache-2.0"}, + }, + }, + &spdx.ListedLicense{Name: "GPL-2.0-only"}, + }, + }, + expected: "(MIT OR Apache-2.0) AND GPL-2.0-only", + }, + { + name: "nested: GPL-2.0 WITH Classpath OR MIT", + info: &spdx.DisjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.WithAdditionOperator{ + SubjectExtendableLicense: &spdx.ListedLicense{Name: "GPL-2.0-only"}, + SubjectAddition: &spdx.ListedLicenseException{AdditionText: "Classpath-exception-2.0"}, + }, + &spdx.ListedLicense{Name: "MIT"}, + }, + }, + expected: "(GPL-2.0-only WITH Classpath-exception-2.0 OR MIT)", + }, + { + name: "nil returns empty", + info: nil, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := v3licenseInfoToExpression(tt.info) + assert.Equal(t, tt.expected, got) + }) + } +} + +func Test_v3toSyftLicenses(t *testing.T) { + tests := []struct { + name string + licenseType license.Type + licenses []spdx.AnyLicenseInfo + expected []string + }{ + { + name: "simple listed license", + licenseType: license.Declared, + licenses: []spdx.AnyLicenseInfo{ + &spdx.ListedLicense{Name: "MIT"}, + }, + expected: []string{"MIT"}, + }, + { + name: "conjunctive set produces single expression", + licenseType: license.Concluded, + licenses: []spdx.AnyLicenseInfo{ + &spdx.ConjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "MIT"}, + &spdx.ListedLicense{Name: "Apache-2.0"}, + }, + }, + }, + expected: []string{"MIT AND Apache-2.0"}, + }, + { + name: "multiple license infos", + licenseType: license.Declared, + licenses: []spdx.AnyLicenseInfo{ + &spdx.ListedLicense{Name: "MIT"}, + &spdx.ListedLicense{Name: "GPL-2.0-only"}, + }, + expected: []string{"MIT", "GPL-2.0-only"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := v3toSyftLicenses(tt.licenseType, tt.licenses...) + require.Len(t, got, len(tt.expected)) + for i, l := range got { + assert.Equal(t, tt.licenseType, l.Type) + assert.Equal(t, tt.expected[i], l.Value) + } + }) + } +} + +func Test_v3parseSPDXLicenses(t *testing.T) { + p := &spdx.Package{Name: "test-pkg"} + + concludedLicense := &spdx.ListedLicense{Name: "MIT"} + declaredLicense := &spdx.ConjunctiveLicenseSet{ + Members: spdx.LicenseInfoList{ + &spdx.ListedLicense{Name: "Apache-2.0"}, + &spdx.ListedLicense{Name: "BSD-3-Clause"}, + }, + } + + relationships := ptrMap[[]spdx.AnyRelationship]{} + relationships.Set(p, []spdx.AnyRelationship{ + &spdx.Relationship{ + Type: spdx.RelationshipType_HasConcludedLicense, + From: p, + To: spdx.ElementList{concludedLicense}, + }, + &spdx.Relationship{ + Type: spdx.RelationshipType_HasDeclaredLicense, + From: p, + To: spdx.ElementList{declaredLicense}, + }, + }) + + licenses := v3parseSPDXLicenses(relationships, p) + + require.Len(t, licenses, 2) + + var concluded, declared []pkg.License + for _, l := range licenses { + switch l.Type { + case license.Concluded: + concluded = append(concluded, l) + case license.Declared: + declared = append(declared, l) + } + } + + require.Len(t, concluded, 1) + assert.Equal(t, "MIT", concluded[0].Value) + + require.Len(t, declared, 1) + assert.Equal(t, "Apache-2.0 AND BSD-3-Clause", declared[0].Value) +} diff --git a/syft/format/encoders_collection.go b/syft/format/encoders_collection.go index ffe7c59cc..37c5a8975 100644 --- a/syft/format/encoders_collection.go +++ b/syft/format/encoders_collection.go @@ -14,12 +14,22 @@ import ( ) type EncoderCollection struct { - encoders []sbom.FormatEncoder + defaultVersions map[sbom.FormatID]string + encoders []sbom.FormatEncoder } func NewEncoderCollection(encoders ...sbom.FormatEncoder) *EncoderCollection { + defaultVersions := map[sbom.FormatID]string{} + for _, encoder := range encoders { + if defaultVersion, ok := encoder.(defaultVersionIndicator); ok { + if defaultVersion.DefaultVersion() { + defaultVersions[encoder.ID()] = encoder.Version() + } + } + } return &EncoderCollection{ - encoders: encoders, + defaultVersions: defaultVersions, + encoders: encoders, } } @@ -77,13 +87,20 @@ func (e EncoderCollection) Get(name string, version string) sbom.FormatEncoder { var mostRecentFormat sbom.FormatEncoder for _, f := range e.encoders { + defaultVersion := e.defaultVersions[f.ID()] log.WithFields("name", f.ID(), "version", f.Version(), "aliases", f.Aliases()).Trace("considering format") names := []string{string(f.ID())} names = append(names, f.Aliases()...) for _, n := range names { - if cleanFormatName(n) == name && versionMatches(f.Version(), version) { - if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() { - mostRecentFormat = f + if cleanFormatName(n) == name { + // if the name or alias matches, and the requested version is any-version, use the default + if version == sbom.AnyVersion && defaultVersion != "" { + version = defaultVersion + } + if versionMatches(f.Version(), version) { + if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() { + mostRecentFormat = f + } } } } @@ -141,3 +158,9 @@ func Encode(s sbom.SBOM, f sbom.FormatEncoder) ([]byte, error) { return buff.Bytes(), nil } + +type defaultVersionIndicator interface { + // DefaultVersion a sbom.FormatEncoder may indicate it is the default version to be used + // when no version is specified for the type + DefaultVersion() bool +} diff --git a/syft/format/encoders_collection_test.go b/syft/format/encoders_collection_test.go index 3e8128fb6..3bd5256c8 100644 --- a/syft/format/encoders_collection_test.go +++ b/syft/format/encoders_collection_test.go @@ -4,7 +4,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/format/internal/spdxutil" "github.com/anchore/syft/syft/sbom" ) @@ -109,3 +111,76 @@ func Test_versionMatches(t *testing.T) { }) } } + +func Test_EncoderCollection_Get(t *testing.T) { + tests := []struct { + name string + searchName string + searchVersion string + expectedID sbom.FormatID + expectedVersion string + }{ + { + name: "explicit name and version", + searchName: "spdx-json", + searchVersion: "2.3", + expectedID: spdxutil.JSONFormatID, + expectedVersion: "2.3", + }, + { + name: "explicit name without version gets default", + searchName: "spdx-json", + searchVersion: "", + expectedID: spdxutil.JSONFormatID, + expectedVersion: "2.3", + }, + { + name: "alias name with version", + searchName: "spdx", + searchVersion: "2.2", + expectedID: spdxutil.TagValueFormatID, + expectedVersion: "2.2", + }, + { + name: "alias name without version gets default", + searchName: "spdx", + searchVersion: "", + expectedID: spdxutil.TagValueFormatID, + expectedVersion: "2.3", + }, + { + name: "invalid name gets nothing", + searchName: "json-spdx", + searchVersion: "2.3", + expectedID: "", + }, + { + name: "invalid version gets nothing", + searchName: "spdx-json", + searchVersion: "2.0", + expectedID: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + config := DefaultEncodersConfig() + // ensure SPDX default is 2.3 for test + config.SPDXJSON.DefaultVersion = "2.3" + encoders, err := config.Encoders() + require.NoError(t, err) + collection := NewEncoderCollection(encoders...) + result := collection.Get(test.searchName, test.searchVersion) + + if test.expectedID != "" { + require.NotNil(t, result, "expected to find encoder but got nil") + if result != nil { + require.Equal(t, test.expectedID, result.ID()) + require.Equal(t, test.expectedVersion, result.Version()) + } + } else { + require.Nil(t, result, "expected nil but found encoder") + } + }) + } +} diff --git a/syft/format/internal/spdxutil/versions.go b/syft/format/internal/spdxutil/versions.go index 5718e5d2c..c7df85d0f 100644 --- a/syft/format/internal/spdxutil/versions.go +++ b/syft/format/internal/spdxutil/versions.go @@ -4,9 +4,10 @@ import ( "github.com/anchore/syft/syft/sbom" ) -const DefaultVersion = "2.3" - const ( + DefaultVersion = "2.3" + V3_0 = "3.0" + JSONFormatID sbom.FormatID = "spdx-json" TagValueFormatID sbom.FormatID = "spdx-tag-value" ) @@ -19,7 +20,10 @@ func SupportedVersions(id sbom.FormatID) []string { if id != JSONFormatID { // JSON format is not supported in v2.1 - return append([]string{"2.1"}, versions...) + versions = append([]string{"2.1"}, versions...) + } else { + // is JSON, v3 only supported in JSON format: + versions = append(versions, V3_0) } return versions diff --git a/syft/format/spdxjson/decoder.go b/syft/format/spdxjson/decoder.go index 5f38ce7ca..01e61ddb5 100644 --- a/syft/format/spdxjson/decoder.go +++ b/syft/format/spdxjson/decoder.go @@ -44,6 +44,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) return nil, "", "", fmt.Errorf("unable to seek to start of SPDX JSON SBOM: %+v", err) } + if strings.HasPrefix(version, "3") { + return decodeSpdx3(version, reader) + } + doc, err := spdxJson.Read(reader) if err != nil { return nil, id, version, fmt.Errorf("unable to decode spdx json: %w", err) @@ -62,11 +66,6 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) { return "", "" } - if _, err := reader.Seek(0, io.SeekStart); err != nil { - log.Debugf("unable to seek to start of SPDX JSON SBOM: %+v", err) - return "", "" - } - // Example JSON document // { // "spdxVersion": "SPDX-2.3", @@ -85,7 +84,18 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) { id, version := getFormatInfo(doc.SPDXVersion) if version == "" || id != ID { - // not a spdx json document that we support + // not a spdx 2 json document that we support, check for v3 + + if _, err = reader.Seek(0, io.SeekStart); err != nil { + log.Debugf("unable to seek to start of SPDX JSON SBOM: %+v", err) + return "", "" + } + + id3, version3 := identifySpdx3(reader) + if id3 != "" && version3 != "" { + return id3, version3 + } + return "", "" } diff --git a/syft/format/spdxjson/encoder.go b/syft/format/spdxjson/encoder.go index 25cd699fb..9ce7476ac 100644 --- a/syft/format/spdxjson/encoder.go +++ b/syft/format/spdxjson/encoder.go @@ -9,6 +9,7 @@ import ( "github.com/spdx/tools-golang/spdx/v2/v2_1" "github.com/spdx/tools-golang/spdx/v2/v2_2" "github.com/spdx/tools-golang/spdx/v2/v2_3" + "github.com/spdx/tools-golang/spdx/v3/v3_0" "github.com/anchore/syft/syft/format/common/spdxhelpers" "github.com/anchore/syft/syft/format/internal/spdxutil" @@ -22,8 +23,9 @@ func SupportedVersions() []string { } type EncoderConfig struct { - Version string - Pretty bool // don't include spaces and newlines; same as jq -c + Version string + Pretty bool // don't include spaces and newlines; same as jq -c + DefaultVersion string } type encoder struct { @@ -38,8 +40,9 @@ func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) { func DefaultEncoderConfig() EncoderConfig { return EncoderConfig{ - Version: spdxutil.DefaultVersion, - Pretty: false, + DefaultVersion: spdxutil.DefaultVersion, + Version: spdxutil.DefaultVersion, + Pretty: false, } } @@ -68,6 +71,7 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error { doc := v2_1.Document{} err = convert.Document(latestDoc, &doc) encodeDoc = doc + case "2.2": doc := v2_2.Document{} err = convert.Document(latestDoc, &doc) @@ -77,6 +81,11 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error { doc := v2_3.Document{} err = convert.Document(latestDoc, &doc) encodeDoc = doc + + case spdxutil.V3_0: + doc := &v3_0.Document{} + err = convert.Document(latestDoc, doc) + encodeDoc = doc default: return fmt.Errorf("unsupported SPDX version %q", e.cfg.Version) } @@ -95,3 +104,7 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error { return enc.Encode(encodeDoc) } + +func (e encoder) DefaultVersion() bool { + return e.cfg.DefaultVersion == e.cfg.Version +} diff --git a/syft/format/spdxjson/encoder_test.go b/syft/format/spdxjson/encoder_test.go index 6698be490..be4680942 100644 --- a/syft/format/spdxjson/encoder_test.go +++ b/syft/format/spdxjson/encoder_test.go @@ -216,6 +216,10 @@ func redactor(values ...string) testutil.Redactor { // each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing `"documentNamespace":\s+"[^"]*"`: `"documentNamespace":"redacted"`, + // spdx3 IDs are URI with the documentnamespace spdxId containing a UID; namespace is the equivalent documentNamespace with the same value + `"spdxId":\s+"[^"]*"`: `"spdxId":"https://redacted"`, + `"namespace":\s+"[^"]*"`: `"namespace":"https://redacted/"`, + // the license list will be updated periodically, the value here should not be directly tested in snapshot tests `"licenseListVersion":\s+"[^"]*"`: `"licenseListVersion":"redacted"`, }, @@ -239,12 +243,14 @@ func TestSupportedVersions(t *testing.T) { "2.2": 2, // the source-to-package relationships can be removed since the primaryPackagePurpose info is available in 2.3 "2.3": 0, + "3.0": 0, } pkgCountOffsetPerVersion := map[string]int{ "2.1": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3 "2.2": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3 "2.3": 0, // the source package can be removed since the primaryPackagePurpose info is available + "3.0": 0, } for _, enc := range encs { diff --git a/syft/format/spdxjson/spdx3_decoder.go b/syft/format/spdxjson/spdx3_decoder.go new file mode 100644 index 000000000..47eb7206a --- /dev/null +++ b/syft/format/spdxjson/spdx3_decoder.go @@ -0,0 +1,63 @@ +package spdxjson + +import ( + "encoding/json" + "fmt" + "io" + "regexp" + + "github.com/spdx/tools-golang/spdx/v3/v3_0" + + "github.com/anchore/syft/syft/format/common/spdxhelpers" + "github.com/anchore/syft/syft/format/internal/spdxutil" + "github.com/anchore/syft/syft/sbom" +) + +var spdx3_0contextRegex = spdxContextRegex(spdxutil.V3_0) + +func decodeSpdx3(version string, reader io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) { + switch version { + case spdxutil.V3_0: + doc := v3_0.NewDocument(v3_0.ProfileIdentifierType_Software, "", nil, nil) + err := doc.FromJSON(reader) + if err != nil { + return nil, "", "", err + } + sb, err := spdxhelpers.ToSyftModelV3(doc) + return sb, spdxutil.JSONFormatID, spdxutil.V3_0, err + default: + return nil, "", "", fmt.Errorf("unsupported version: %v", version) + } +} + +func identifySpdx3(reader io.Reader) (sbom.FormatID, string) { + type Document struct { + Context string `json:"@context"` + } + + dec := json.NewDecoder(reader) + + var doc Document + if err := dec.Decode(&doc); err != nil { + // maybe not json? maybe not valid? doesn't matter, we won't process it. + return "", "" + } + + formatID := sbom.FormatID("") + spdxVersion := "" + + switch { + case doc.Context == "": + case spdx3_0contextRegex.MatchString(doc.Context): + formatID = spdxutil.JSONFormatID + spdxVersion = spdxutil.V3_0 + default: + } + + return formatID, spdxVersion +} + +func spdxContextRegex(minorVersion string) *regexp.Regexp { + // today this is "3.0.1", but is likely to be changed to only include the minor version "3.0" + return regexp.MustCompile(regexp.QuoteMeta("https://spdx.org/rdf/") + minorVersion + `(\.\d+)?` + regexp.QuoteMeta("/spdx-context.jsonld")) +} diff --git a/syft/format/spdxjson/spdx3_decoder_test.go b/syft/format/spdxjson/spdx3_decoder_test.go new file mode 100644 index 000000000..81708294e --- /dev/null +++ b/syft/format/spdxjson/spdx3_decoder_test.go @@ -0,0 +1,426 @@ +package spdxjson + +import ( + "bytes" + "context" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/format/internal/spdxutil" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +// TestSPDX3JSONRoundTrip_AllPackageTypes encodes a syft SBOM as SPDX 3.0 JSON and decodes it back, asserting +// (via a full struct diff) that every package field representable in SPDX 3.0 survives the trip. +// +// Each case provides the package to encode (input) and the package expected after decoding +// (want). They differ because some information is not representable in the SPDX 3.0 model: +// - Locations / FoundBy are not encoded per-package (ignored in the diff below). +// - The package ID is derived from the SPDX element ID rather than the original syft ID +// (the unexported id field is ignored in the diff below). +// - Type and Language are reconstructed from the package URL type. +// - Metadata is only reconstructed for the package types with dedicated handling in the +// SPDX encoder/decoder (apk, deb, rpm, java-archive, go-module); for all other types the +// decoded package carries no metadata. +func TestSPDX3JSONRoundTrip_AllPackageTypes(t *testing.T) { + ctx := context.Background() + + license := func(value string) pkg.LicenseSet { + return pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, value)) + } + cpes := func(value string) []cpe.CPE { + return []cpe.CPE{cpe.Must(value, cpe.DeclaredSource)} + } + + cases := []struct { + name string + input pkg.Package + want pkg.Package + }{ + { + name: "python package (no reconstructed metadata)", + input: pkg.Package{ + Name: "package-python", + Version: "1.0.1", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: license("MIT"), + CPEs: cpes("cpe:2.3:a:python:package-python:1.0.1:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/python")), + PURL: "pkg:pypi/package-python@1.0.1", + Metadata: pkg.PythonPackage{Name: "package-python", Version: "1.0.1"}, + }, + want: pkg.Package{ + Name: "package-python", + Version: "1.0.1", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: license("MIT"), + CPEs: cpes("cpe:2.3:a:python:package-python:1.0.1:*:*:*:*:*:*:*"), + PURL: "pkg:pypi/package-python@1.0.1", + }, + }, + { + name: "npm package (no reconstructed metadata)", + input: pkg.Package{ + Name: "package-npm", + Version: "2.0.1", + Type: pkg.NpmPkg, + Language: pkg.JavaScript, + Licenses: license("Apache-2.0"), + CPEs: cpes("cpe:2.3:a:npm:package-npm:2.0.1:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/npm")), + PURL: "pkg:npm/package-npm@2.0.1", + Metadata: pkg.NpmPackage{Name: "package-npm", Version: "2.0.1"}, + }, + want: pkg.Package{ + Name: "package-npm", + Version: "2.0.1", + Type: pkg.NpmPkg, + Language: pkg.JavaScript, + Licenses: license("Apache-2.0"), + CPEs: cpes("cpe:2.3:a:npm:package-npm:2.0.1:*:*:*:*:*:*:*"), + PURL: "pkg:npm/package-npm@2.0.1", + }, + }, + { + name: "apk package", + input: pkg.Package{ + Name: "package-apk", + Version: "3.0", + Type: pkg.ApkPkg, + Licenses: license("GPL-2.0-only"), + CPEs: cpes("cpe:2.3:a:alpine:package-apk:3.0:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/apk")), + PURL: "pkg:apk/alpine/package-apk@3.0?arch=x86_64&upstream=apk-origin", + Metadata: pkg.ApkDBEntry{ + Package: "package-apk", + OriginPackage: "apk-origin", + Maintainer: "Alpine Maintainer", + Version: "3.0", + Architecture: "x86_64", + Description: "the apk package", + }, + }, + want: pkg.Package{ + Name: "package-apk", + Version: "3.0", + Type: pkg.ApkPkg, + Licenses: license("GPL-2.0-only"), + CPEs: cpes("cpe:2.3:a:alpine:package-apk:3.0:*:*:*:*:*:*:*"), + PURL: "pkg:apk/alpine/package-apk@3.0?arch=x86_64&upstream=apk-origin", + Metadata: pkg.ApkDBEntry{ + Package: "package-apk", + OriginPackage: "apk-origin", + Maintainer: "Alpine Maintainer", + Version: "3.0", + Architecture: "x86_64", + Description: "the apk package", + }, + }, + }, + { + name: "deb package", + input: pkg.Package{ + Name: "package-deb", + Version: "4.0", + Type: pkg.DebPkg, + Licenses: license("LGPL-2.1-only"), + CPEs: cpes("cpe:2.3:a:debian:package-deb:4.0:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/deb")), + PURL: "pkg:deb/debian/package-deb@4.0?arch=amd64&upstream=deb-src%404.1", + Metadata: pkg.DpkgDBEntry{ + Package: "package-deb", + Source: "deb-src", + Version: "4.0", + SourceVersion: "4.1", + Architecture: "amd64", + Maintainer: "Debian Maintainer", + }, + }, + want: pkg.Package{ + Name: "package-deb", + Version: "4.0", + Type: pkg.DebPkg, + Licenses: license("LGPL-2.1-only"), + CPEs: cpes("cpe:2.3:a:debian:package-deb:4.0:*:*:*:*:*:*:*"), + PURL: "pkg:deb/debian/package-deb@4.0?arch=amd64&upstream=deb-src%404.1", + Metadata: pkg.DpkgDBEntry{ + Package: "package-deb", + Source: "deb-src", + Version: "4.0", + SourceVersion: "4.1", + Architecture: "amd64", + Maintainer: "Debian Maintainer", + }, + }, + }, + { + name: "rpm package", + input: pkg.Package{ + Name: "package-rpm", + Version: "5.0", + Type: pkg.RpmPkg, + Licenses: license("BSD-3-Clause"), + CPEs: cpes("cpe:2.3:a:redhat:package-rpm:5.0:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/rpm")), + PURL: "pkg:rpm/redhat/package-rpm@5.0?arch=x86_64&upstream=rpm-src-5.0", + Metadata: pkg.RpmDBEntry{ + Name: "package-rpm", + Version: "5.0", + Arch: "x86_64", + SourceRpm: "rpm-src-5.0", + Vendor: "RedHat", + }, + }, + want: pkg.Package{ + Name: "package-rpm", + Version: "5.0", + Type: pkg.RpmPkg, + Licenses: license("BSD-3-Clause"), + CPEs: cpes("cpe:2.3:a:redhat:package-rpm:5.0:*:*:*:*:*:*:*"), + PURL: "pkg:rpm/redhat/package-rpm@5.0?arch=x86_64&upstream=rpm-src-5.0", + Metadata: pkg.RpmDBEntry{ + Name: "package-rpm", + Version: "5.0", + Arch: "x86_64", + SourceRpm: "rpm-src-5.0", + Vendor: "RedHat", + }, + }, + }, + { + name: "java package", + input: pkg.Package{ + Name: "package-java", + Version: "6.0", + Type: pkg.JavaPkg, + Language: pkg.Java, + Licenses: license("EPL-2.0"), + CPEs: cpes("cpe:2.3:a:example:package-java:6.0:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/java")), + PURL: "pkg:maven/com.example/package-java@6.0", + Metadata: pkg.JavaArchive{ + ArchiveDigests: []file.Digest{ + {Algorithm: "sha1", Value: "3b4ab96c371d913e2a88c269844b6c5fb5cbe761"}, + }, + }, + }, + want: pkg.Package{ + Name: "package-java", + Version: "6.0", + Type: pkg.JavaPkg, + Language: pkg.Java, + Licenses: license("EPL-2.0"), + CPEs: cpes("cpe:2.3:a:example:package-java:6.0:*:*:*:*:*:*:*"), + PURL: "pkg:maven/com.example/package-java@6.0", + Metadata: pkg.JavaArchive{ + ArchiveDigests: []file.Digest{ + {Algorithm: "sha1", Value: "3b4ab96c371d913e2a88c269844b6c5fb5cbe761"}, + }, + }, + }, + }, + { + name: "go module package", + input: pkg.Package{ + Name: "package-go", + Version: "7.0", + Type: pkg.GoModulePkg, + Language: pkg.Go, + Licenses: license("MPL-2.0"), + CPEs: cpes("cpe:2.3:a:example:package-go:7.0:*:*:*:*:*:*:*"), + Locations: file.NewLocationSet(file.NewLocation("/go")), + PURL: "pkg:golang/example.com/package-go@7.0", + Metadata: pkg.GolangBinaryBuildinfoEntry{ + H1Digest: "h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + }, + }, + want: pkg.Package{ + Name: "package-go", + Version: "7.0", + Type: pkg.GoModulePkg, + Language: pkg.Go, + Licenses: license("MPL-2.0"), + CPEs: cpes("cpe:2.3:a:example:package-go:7.0:*:*:*:*:*:*:*"), + PURL: "pkg:golang/example.com/package-go@7.0", + Metadata: pkg.GolangBinaryBuildinfoEntry{ + H1Digest: "h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + }, + }, + }, + } + + var inputs []pkg.Package + var want []pkg.Package + for _, c := range cases { + in := c.input + in.SetID() + inputs = append(inputs, in) + want = append(want, c.want) + } + + subject := sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(inputs...), + }, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + }, + Source: source.Description{ + Metadata: source.DirectoryMetadata{Path: "/home/app", Base: "/home/app"}, + }, + } + + cfg := DefaultEncoderConfig() + cfg.Pretty = true + cfg.Version = spdxutil.V3_0 + + enc, err := NewFormatEncoderWithConfig(cfg) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, enc.Encode(&buf, subject)) + + dec := NewFormatDecoder() + + id, version := dec.Identify(bytes.NewReader(buf.Bytes())) + require.Equal(t, ID, id) + require.Equal(t, spdxutil.V3_0, version) + + s, decodeID, decodeVersion, err := dec.Decode(bytes.NewReader(buf.Bytes())) + require.NoError(t, err) + require.NotNil(t, s) + require.Equal(t, ID, decodeID) + require.Equal(t, spdxutil.V3_0, decodeVersion) + + // the directory source should be recognized as the document root and not surface as a package + require.Equal(t, len(want), s.Artifacts.Packages.PackageCount()) + + got := s.Artifacts.Packages.Sorted() + sortByName(want) + sortByName(got) + + diff := cmp.Diff(want, got, + cmpopts.IgnoreUnexported(pkg.Package{}, pkg.LicenseSet{}, file.LocationSet{}), + cmpopts.IgnoreFields(pkg.Package{}, "Locations", "FoundBy"), + ) + require.Empty(t, diff, "decoded packages differ from expected (-want +got):\n%s", diff) +} + +func sortByName(pkgs []pkg.Package) { + sort.Slice(pkgs, func(i, j int) bool { + return pkgs[i].Name < pkgs[j].Name + }) +} + +// TestSPDX3JSONRoundTrip_FullSBOM encodes a more complete SBOM (a container image source, several +// packages, and relationships between them) as SPDX 3.0 JSON and decodes it back, verifying that +// the source description and the package relationships survive the round trip. +func TestSPDX3JSONRoundTrip_FullSBOM(t *testing.T) { + newPkg := func(name string) pkg.Package { + p := pkg.Package{ + Name: name, + Version: "1.0", + Type: pkg.NpmPkg, + Language: pkg.JavaScript, + PURL: "pkg:npm/" + name + "@1.0", + Locations: file.NewLocationSet( + file.NewLocation("/" + name), + ), + } + p.SetID() + return p + } + + app := newPkg("app") + lib := newPkg("lib") + dep := newPkg("dep") + + subject := sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(app, lib, dep), + }, + Relationships: []artifact.Relationship{ + // app contains lib + {From: app, To: lib, Type: artifact.ContainsRelationship}, + // dep is a dependency of lib + {From: dep, To: lib, Type: artifact.DependencyOfRelationship}, + }, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + }, + Source: source.Description{ + Name: "some-image", + Version: "some-tag", + Supplier: "some-supplier", + Metadata: source.ImageMetadata{ + UserInput: "some-image:some-tag", + ManifestDigest: "sha256:abcdef0123456789", + Architecture: "amd64", + }, + }, + } + + cfg := DefaultEncoderConfig() + cfg.Pretty = true + cfg.Version = spdxutil.V3_0 + + enc, err := NewFormatEncoderWithConfig(cfg) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, enc.Encode(&buf, subject)) + + dec := NewFormatDecoder() + s, decodeID, decodeVersion, err := dec.Decode(bytes.NewReader(buf.Bytes())) + require.NoError(t, err) + require.NotNil(t, s) + require.Equal(t, ID, decodeID) + require.Equal(t, spdxutil.V3_0, decodeVersion) + + // the container image source is decoded as the document root rather than a package + require.Equal(t, 3, s.Artifacts.Packages.PackageCount()) + + // source information survives, and the source is not surfaced as a package + // (the ID is re-derived from the SPDX element ID and is therefore not compared) + require.Equal(t, "some-image", s.Source.Name) + require.Equal(t, "some-tag", s.Source.Version) + require.Equal(t, "some-supplier", s.Source.Supplier) + require.IsType(t, source.ImageMetadata{}, s.Source.Metadata) + imageMetadata := s.Source.Metadata.(source.ImageMetadata) + // the user input is reconstructed from the image name and tag (version) + require.Equal(t, "some-image:some-tag", imageMetadata.UserInput) + require.Equal(t, "sha256:abcdef0123456789", imageMetadata.ManifestDigest) + + // relationships between packages survive, including direction + type rel struct { + from string + to string + typ artifact.RelationshipType + } + var got []rel + for _, r := range s.Relationships { + from, fromOk := r.From.(pkg.Package) + to, toOk := r.To.(pkg.Package) + require.True(t, fromOk, "relationship from should be a package: %+v", r.From) + require.True(t, toOk, "relationship to should be a package: %+v", r.To) + got = append(got, rel{from: from.Name, to: to.Name, typ: r.Type}) + } + + require.ElementsMatch(t, []rel{ + {from: "app", to: "lib", typ: artifact.ContainsRelationship}, + {from: "dep", to: "lib", typ: artifact.DependencyOfRelationship}, + }, got) +} diff --git a/syft/format/spdxjson/testdata/identify/3.0.json b/syft/format/spdxjson/testdata/identify/3.0.json new file mode 100644 index 000000000..9b082f57d --- /dev/null +++ b/syft/format/spdxjson/testdata/identify/3.0.json @@ -0,0 +1,141 @@ +{ + "@context" : "https://spdx.org/rdf/3.0.1/spdx-context.jsonld", + "@graph" : [ { + "@id" : "_:creationInfo_0", + "type" : "CreationInfo", + "specVersion" : "3.0.1", + "createdBy" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0" ], + "createdUsing" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd2", "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd1" ], + "created" : "2021-08-26T01:46:00Z" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd23", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/document0", + "type" : "SpdxDocument", + "dataLicense" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd24", + "rootElement" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4" ], + "name" : "hello", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd1", + "type" : "Tool", + "name" : "github.com/spdx/tools-golang/builder", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd2", + "type" : "Tool", + "name" : "github.com/spdx/tools-golang/idsearcher", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "GPL-3.0-or-later", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd20", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "NOASSERTION", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd24", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "CC0-1.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd14", + "type" : "LifecycleScopedRelationship", + "relationshipType" : "usesTool", + "scope" : "build", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13" ], + "completeness" : "noAssertion", + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0", + "type" : "Person", + "externalIdentifier" : [ { + "type" : "ExternalIdentifier", + "identifier" : "steve@swinslow.net", + "externalIdentifierType" : "email" + } ], + "name" : "Steve Winslow", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd6", + "type" : "software_File", + "software_copyrightText" : "Copyright Contributors to the spdx-examples project.", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "md5", + "hashValue" : "935054fe899ca782e11003bbae5e166c" + }, { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "20862a6d08391d07d09344029533ec644fac6b21" + }, { + "type" : "Hash", + "algorithm" : "sha256", + "hashValue" : "b4e5ca56d1f9110ca94ed0bf4e6d9ac11c2186eb7cd95159c6fdb50e8db5a823" + } ], + "name" : "./src/hello.c", + "software_primaryPurpose" : "source", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11", + "type" : "software_File", + "software_copyrightText" : "NOASSERTION", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "20291a81ef065ff891b537b64d4fdccaf6f5ac02" + }, { + "type" : "Hash", + "algorithm" : "sha256", + "hashValue" : "83a33ff09648bb5fc5272baca88cf2b59fd81ac4cc6817b86998136af368708e" + }, { + "type" : "Hash", + "algorithm" : "md5", + "hashValue" : "08a12c966d776864cc1eb41fd03c3c3d" + } ], + "name" : "./build/hello", + "contentType" : "application/octet-stream", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13", + "type" : "software_File", + "software_copyrightText" : "NOASSERTION", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "69a2e85696fff1865c3f0686d6c3824b59915c80" + }, { + "type" : "Hash", + "algorithm" : "sha256", + "hashValue" : "5da19033ba058e322e21c90e6d6d859c90b1b544e7840859c12cae5da005e79c" + }, { + "type" : "Hash", + "algorithm" : "md5", + "hashValue" : "559424589a4f3f75fd542810473d8bc1" + } ], + "name" : "./src/Makefile", + "software_primaryPurpose" : "source", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "type" : "software_Package", + "software_copyrightText" : "NOASSERTION", + "software_downloadLocation" : "git+https://github.com/swinslow/spdx-examples.git#example1/content", + "verifiedUsing" : [ { + "type" : "PackageVerificationCode", + "algorithm" : "sha1", + "hashValue" : "9d20237bb72087e87069f96afb41c6ca2fa2a342" + } ], + "name" : "hello", + "creationInfo" : "_:creationInfo_0" + } ] +} \ No newline at end of file diff --git a/test/cli/all_formats_convertible_test.go b/test/cli/all_formats_convertible_test.go index 8b5a27798..dad736913 100644 --- a/test/cli/all_formats_convertible_test.go +++ b/test/cli/all_formats_convertible_test.go @@ -29,6 +29,7 @@ func TestAllFormatsConvertable(t *testing.T) { {to: "spdx-json", from: "cyclonedx-json"}, {to: "cyclonedx-json", from: "syft-json"}, {to: "cyclonedx-json", from: "spdx-json"}, + {to: "spdx-json@3", from: "spdx-tag-value@2.1"}, } for _, test := range tests { diff --git a/test/cli/all_formats_expressible_test.go b/test/cli/all_formats_expressible_test.go index f13d4c883..fe1bd4564 100644 --- a/test/cli/all_formats_expressible_test.go +++ b/test/cli/all_formats_expressible_test.go @@ -65,6 +65,10 @@ func Test_formatVersionsExpressible(t *testing.T) { format: "spdx-json@2.3", assertion: assertInOutput(`"spdxVersion":"SPDX-2.3"`), }, + { + format: "spdx-json@3.0", + assertion: assertInOutput(`https://spdx.org/rdf/3.0`), + }, } for _, test := range tests { diff --git a/test/cli/convert_cmd_test.go b/test/cli/convert_cmd_test.go index 944eb4876..04df14910 100644 --- a/test/cli/convert_cmd_test.go +++ b/test/cli/convert_cmd_test.go @@ -28,6 +28,7 @@ func TestConvertCmd(t *testing.T) { }{ {from: "syft-json", to: "spdx-tag-value", expect: mustEncoder(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig()))}, {from: "syft-json", to: "spdx-json", expect: mustEncoder(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig()))}, + {from: "syft-json", to: "spdx-json@3.0", expect: mustEncoder(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig()))}, {from: "syft-json", to: "cyclonedx-json", expect: mustEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig()))}, {from: "syft-json", to: "cyclonedx-xml", expect: mustEncoder(cyclonedxxml.NewFormatEncoderWithConfig(cyclonedxxml.DefaultEncoderConfig()))}, } diff --git a/test/cli/spdx_json_schema_test.go b/test/cli/spdx_json_schema_test.go index 61c6972a3..4df213c1d 100644 --- a/test/cli/spdx_json_schema_test.go +++ b/test/cli/spdx_json_schema_test.go @@ -31,7 +31,7 @@ func TestSPDXJSONSchema(t *testing.T) { { name: "scan:image:docker-archive:pkg-coverage", subcommand: "scan", - args: []string{"-o", "spdx-json"}, + args: []string{"-o", "spdx-json@2.3"}, fixture: imageFixture, }, { diff --git a/test/cli/spdx_tooling_validation_test.go b/test/cli/spdx_tooling_validation_test.go index 5d1444007..4eaa3ba89 100644 --- a/test/cli/spdx_tooling_validation_test.go +++ b/test/cli/spdx_tooling_validation_test.go @@ -2,23 +2,20 @@ package cli import ( "fmt" - "os" "os/exec" - "path" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" +) - "github.com/anchore/stereoscope/pkg/imagetest" +const ( + validatorV2 = "ghcr.io/spdx/tools-java/tools-java:v1.1.8@sha256:c3b9e848083132e03b30302576b9b51adffd454f43c786f1708cc37c0861a2aa" + validatorV3 = "ghcr.io/spdx/tools-java/tools-java:v2.0.4@sha256:15062f85b4be9688c7bf42df34ad6b84e084ed46e262e1f2dc1603795de9f7b4" ) func TestSpdxValidationTooling(t *testing.T) { - // note: the external tooling requires that the daemon explicitly has the image loaded, not just that - // we can get the image from a cache tar. - imgTag := imagetest.LoadFixtureImageIntoDocker(t, "image-java-spdx-tools") - images := []string{ "alpine:3.17.3@sha256:b6ca290b6b4cdcca5b3db3ffa338ee0285c11744b4a6abaa9627746ee3291d8d", "photon:3.0@sha256:888675e193418d924feea262cf639c46532b63c2027a39fd3ac75383b3c1130e", @@ -32,43 +29,44 @@ func TestSpdxValidationTooling(t *testing.T) { } tests := []struct { - name string - syftArgs []string - images []string - setup func(t *testing.T) - env map[string]string + name string + format string + validator string }{ { - name: "spdx validation tooling tag value", - syftArgs: []string{"scan", "-o", "spdx"}, - images: images, - env: env, + name: "spdx 2.3 validation tooling tag value", + format: "spdx", }, { - name: "spdx validation tooling json", - syftArgs: []string{"scan", "-o", "spdx-json"}, - images: images, - env: env, + name: "spdx 2.3 validation tooling json", + format: "spdx-json", }, { - name: "spdx validation tooling tag value", - syftArgs: []string{"scan", "-o", "spdx@2.2"}, - images: images, - env: env, + name: "spdx 3.0 validation tooling json", + format: "spdx-json@3.0", + validator: validatorV3, }, { - name: "spdx validation tooling json", - syftArgs: []string{"scan", "-o", "spdx-json@2.2"}, - images: images, - env: env, + name: "spdx 2.2 validation tooling tag value", + format: "spdx@2.2", + }, + { + name: "spdx 2.2 validation tooling json", + format: "spdx-json@2.2", }, } - for _, test := range tests { - for _, image := range test.images { - t.Run(test.name+"_"+image, func(t *testing.T) { + for _, image := range images { + syftJsonFile := filepath.Join(t.TempDir(), "sbom.syft.json") - args := append(test.syftArgs, image) + cmd, _, stderr := runSyft(t, env, "-o", "syft-json", "--file", syftJsonFile, image) + if cmd.ProcessState.ExitCode() != 0 { + t.Fatalf("failed to run syft: %s", stderr) + } + + for _, test := range tests { + t.Run(test.name+"_"+image, func(t *testing.T) { + t.Parallel() var suffix string if strings.Contains(test.name, "json") { @@ -78,30 +76,27 @@ func TestSpdxValidationTooling(t *testing.T) { } dir := t.TempDir() - sbomPath := filepath.Join(dir, fmt.Sprintf("sbom%s", suffix)) + sbomFile := fmt.Sprintf("sbom%s", suffix) + sbomPath := filepath.Join(dir, sbomFile) - args = append(args, "--file", sbomPath) - - cmd, _, stderr := runSyft(t, test.env, args...) + cmd, _, stderr = runSyft(t, nil, "convert", syftJsonFile, "-o", test.format, "--file", sbomPath) if cmd.ProcessState.ExitCode() != 0 { - t.Fatalf("failed to run syft: %s", stderr) + t.Fatalf("failed to run syft convert: %s", stderr) } - cwd, err := os.Getwd() - require.NoError(t, err) + if test.validator == "" { + test.validator = validatorV2 + } // validate against spdx java tooling - fileArg := fmt.Sprintf("DIR=%s", dir) - mountArg := fmt.Sprintf("BASE=%s", path.Base(sbomPath)) - imageArg := fmt.Sprintf("IMAGE=%s", imgTag) - - validateCmd := exec.Command("make", "validate", fileArg, mountArg, imageArg) - validateCmd.Dir = filepath.Join(cwd, "testdata", "image-java-spdx-tools") + validateCmd := exec.Command("docker", "run", "--rm", "-i", "--platform", "linux/amd64", + "-v", dir+":/data", test.validator, "Verify", "/data/"+sbomFile) stdout, stderr, err := runCommand(validateCmd, map[string]string{}) if err != nil { t.Fatalf("invalid SPDX document:%v\nSTDOUT:\n%s\nSTDERR:\n%s", err, stdout, stderr) } + require.Contains(t, stdout, "SPDX Document is valid") }) } } diff --git a/test/cli/testdata/image-java-spdx-tools/Dockerfile b/test/cli/testdata/image-java-spdx-tools/Dockerfile deleted file mode 100644 index 938ca24d0..000000000 --- a/test/cli/testdata/image-java-spdx-tools/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM openjdk:11@sha256:e81b7f317654b0f26d3993e014b04bcb29250339b11b9de41e130feecd4cd43c - -RUN wget --no-verbose https://github.com/spdx/tools-java/releases/download/v1.1.3/tools-java-1.1.3.zip && \ - unzip tools-java-1.1.3.zip && \ - rm tools-java-1.1.3.zip - -ENTRYPOINT ["java", "-jar", "tools-java-1.1.3-jar-with-dependencies.jar"] diff --git a/test/cli/testdata/image-java-spdx-tools/Makefile b/test/cli/testdata/image-java-spdx-tools/Makefile deleted file mode 100644 index a6bcc26ab..000000000 --- a/test/cli/testdata/image-java-spdx-tools/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -all: build validate - -IMAGE := "spdx-java-tools:latest" - -.PHONY: build -build: - docker build -t spdx-java-tools:latest . - -validate: - docker run --rm -v $(DIR):/home/build/ $(IMAGE) Verify /home/build/$(BASE) From 52a4c3b5947823f991fc3a6cfe4fd0946172dfac Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Mon, 22 Jun 2026 11:49:44 -0400 Subject: [PATCH 016/168] feat: elastic beats bin classifier (#4969) Signed-off-by: Rez Moss --- syft/pkg/cataloger/binary/capabilities.yaml | 50 ++++++++++++ .../binary/classifier_cataloger_test.go | 77 +++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 52 +++++++++++++ .../auditbeat/9.4.2/linux-amd64/auditbeat | 8 ++ .../filebeat/8.11.2/linux-amd64/filebeat | 8 ++ .../filebeat/8.18.4/linux-amd64/filebeat | 8 ++ .../filebeat/9.4.2/linux-amd64/filebeat | 8 ++ .../heartbeat/9.4.2/linux-amd64/heartbeat | 8 ++ .../metricbeat/9.4.2/linux-amd64/metricbeat | 8 ++ .../packetbeat/9.4.2/linux-amd64/packetbeat | 8 ++ .../pkg/cataloger/binary/testdata/config.yaml | 58 +++++++++++++- 11 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/auditbeat/9.4.2/linux-amd64/auditbeat create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/filebeat/8.11.2/linux-amd64/filebeat create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/filebeat/8.18.4/linux-amd64/filebeat create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/filebeat/9.4.2/linux-amd64/filebeat create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/heartbeat/9.4.2/linux-amd64/heartbeat create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/metricbeat/9.4.2/linux-amd64/metricbeat create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/packetbeat/9.4.2/linux-amd64/packetbeat diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index bf7e11d04..c98e5c57e 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -786,6 +786,56 @@ catalogers: cpes: - cpe:2.3:a:kubernetes:ingress-nginx:*:*:*:*:*:*:*:* type: BinaryPkg + - method: glob + criteria: + - '**/filebeat' + packages: + - class: filebeat-binary + name: filebeat + purl: pkg:generic/filebeat + cpes: + - cpe:2.3:a:elastic:filebeat:*:*:*:*:*:*:*:* + type: BinaryPkg + - method: glob + criteria: + - '**/metricbeat' + packages: + - class: metricbeat-binary + name: metricbeat + purl: pkg:generic/metricbeat + cpes: + - cpe:2.3:a:elastic:metricbeat:*:*:*:*:*:*:*:* + type: BinaryPkg + - method: glob + criteria: + - '**/heartbeat' + packages: + - class: heartbeat-binary + name: heartbeat + purl: pkg:generic/heartbeat + cpes: + - cpe:2.3:a:elastic:heartbeat:*:*:*:*:*:*:*:* + type: BinaryPkg + - method: glob + criteria: + - '**/packetbeat' + packages: + - class: packetbeat-binary + name: packetbeat + purl: pkg:generic/packetbeat + cpes: + - cpe:2.3:a:elastic:packetbeat:*:*:*:*:*:*:*:* + type: BinaryPkg + - method: glob + criteria: + - '**/auditbeat' + packages: + - class: auditbeat-binary + name: auditbeat + purl: pkg:generic/auditbeat + cpes: + - cpe:2.3:a:elastic:auditbeat:*:*:*:*:*:*:*:* + type: BinaryPkg - method: glob criteria: - '**/elastic-agent' diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index f58963db8..a64ea072b 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -2557,6 +2557,83 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("ingress-nginx-binary"), }, }, + { + logicalFixture: "filebeat/9.4.2/linux-amd64", + expected: pkg.Package{ + Name: "filebeat", + Version: "9.4.2", + Type: "binary", + PURL: "pkg:generic/filebeat@9.4.2", + Locations: locations("filebeat"), + Metadata: metadata("filebeat-binary"), + }, + }, + { + logicalFixture: "filebeat/8.18.4/linux-amd64", + expected: pkg.Package{ + Name: "filebeat", + Version: "8.18.4", + Type: "binary", + PURL: "pkg:generic/filebeat@8.18.4", + Locations: locations("filebeat"), + Metadata: metadata("filebeat-binary"), + }, + }, + { + logicalFixture: "filebeat/8.11.2/linux-amd64", + expected: pkg.Package{ + Name: "filebeat", + Version: "8.11.2", + Type: "binary", + PURL: "pkg:generic/filebeat@8.11.2", + Locations: locations("filebeat"), + Metadata: metadata("filebeat-binary"), + }, + }, + { + logicalFixture: "metricbeat/9.4.2/linux-amd64", + expected: pkg.Package{ + Name: "metricbeat", + Version: "9.4.2", + Type: "binary", + PURL: "pkg:generic/metricbeat@9.4.2", + Locations: locations("metricbeat"), + Metadata: metadata("metricbeat-binary"), + }, + }, + { + logicalFixture: "heartbeat/9.4.2/linux-amd64", + expected: pkg.Package{ + Name: "heartbeat", + Version: "9.4.2", + Type: "binary", + PURL: "pkg:generic/heartbeat@9.4.2", + Locations: locations("heartbeat"), + Metadata: metadata("heartbeat-binary"), + }, + }, + { + logicalFixture: "packetbeat/9.4.2/linux-amd64", + expected: pkg.Package{ + Name: "packetbeat", + Version: "9.4.2", + Type: "binary", + PURL: "pkg:generic/packetbeat@9.4.2", + Locations: locations("packetbeat"), + Metadata: metadata("packetbeat-binary"), + }, + }, + { + logicalFixture: "auditbeat/9.4.2/linux-amd64", + expected: pkg.Package{ + Name: "auditbeat", + Version: "9.4.2", + Type: "binary", + PURL: "pkg:generic/auditbeat@9.4.2", + Locations: locations("auditbeat"), + Metadata: metadata("auditbeat-binary"), + }, + }, { logicalFixture: "elastic-agent/9.4.2/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 9b7680a4d..56f7865de 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -28,6 +28,18 @@ func DefaultClassifiers() []binutils.Classifier { // ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux] `(?m)ruby (?P[0-9]+\.[0-9]+\.[0-9]+((p|preview|rc|dev)[0-9]*)?) `) + // all Elastic Beats (filebeat, metricbeat, heartbeat, packetbeat, auditbeat) + var elasticBeatsMatcher = binutils.MatchAny( + // 9.x: forcestdinsetupTest 9.4.2%s %w (filebeat/metricbeat/auditbeat) + // forcestdinsetupTest 9.4.2input (heartbeat/packetbeat) + m.FileContentsVersionMatcher(`Test (?P[0-9]+\.[0-9]+\.[0-9]+)[a-z%]`), + // 9.x: exportconfigcreateplugin9.4.2-globalclient + // 8.18.x: exportconfigcreateplugin8.18.4globalclient + m.FileContentsVersionMatcher(`plugin(?:output)?(?P[0-9]+\.[0-9]+\.[0-9]+)[-a-z]`), + // 8.11.x: 5m.rate8.11.2-9765625 + m.FileContentsVersionMatcher(`5m\.rate(?P[0-9]+\.[0-9]+\.[0-9]+)-`), + ) + classifiers := []binutils.Classifier{ { Class: "python-binary", @@ -1110,6 +1122,46 @@ func DefaultClassifiers() []binutils.Classifier { PURL: mustPURL("pkg:generic/nginx-ingress-controller@version"), CPEs: singleCPE("cpe:2.3:a:kubernetes:ingress-nginx:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, + { + Class: "filebeat-binary", + FileGlob: "**/filebeat", + EvidenceMatcher: elasticBeatsMatcher, + Package: "filebeat", + PURL: mustPURL("pkg:generic/filebeat@version"), + CPEs: singleCPE("cpe:2.3:a:elastic:filebeat:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "metricbeat-binary", + FileGlob: "**/metricbeat", + EvidenceMatcher: elasticBeatsMatcher, + Package: "metricbeat", + PURL: mustPURL("pkg:generic/metricbeat@version"), + CPEs: singleCPE("cpe:2.3:a:elastic:metricbeat:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "heartbeat-binary", + FileGlob: "**/heartbeat", + EvidenceMatcher: elasticBeatsMatcher, + Package: "heartbeat", + PURL: mustPURL("pkg:generic/heartbeat@version"), + CPEs: singleCPE("cpe:2.3:a:elastic:heartbeat:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "packetbeat-binary", + FileGlob: "**/packetbeat", + EvidenceMatcher: elasticBeatsMatcher, + Package: "packetbeat", + PURL: mustPURL("pkg:generic/packetbeat@version"), + CPEs: singleCPE("cpe:2.3:a:elastic:packetbeat:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "auditbeat-binary", + FileGlob: "**/auditbeat", + EvidenceMatcher: elasticBeatsMatcher, + Package: "auditbeat", + PURL: mustPURL("pkg:generic/auditbeat@version"), + CPEs: singleCPE("cpe:2.3:a:elastic:auditbeat:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, { Class: "elastic-agent-binary", FileGlob: "**/elastic-agent", diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/auditbeat/9.4.2/linux-amd64/auditbeat b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/auditbeat/9.4.2/linux-amd64/auditbeat new file mode 100644 index 000000000..c92847e0f --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/auditbeat/9.4.2/linux-amd64/auditbeat @@ -0,0 +1,8 @@ +name: auditbeat +offset: 106492476 +length: 100 +snippetSha256: 7cfca991c6d073690c48c9489a6929009b866e78d266ecb6f6f2b6f04d9f592f +fileSha256: 81ac1f3ab45bb7b3211cf26f94660ae73fdd257bb4890bdf8e71ecddb5afce10 + +### byte snippet to follow ### +rulesforcestdinsetupTest 9.4.2%s %windexeventint16int32int64uint8arraysliceInts:Ptrs:sse41sse42ssse3 \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/filebeat/8.11.2/linux-amd64/filebeat b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/filebeat/8.11.2/linux-amd64/filebeat new file mode 100644 index 000000000..48e03e85a --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/filebeat/8.11.2/linux-amd64/filebeat @@ -0,0 +1,8 @@ +name: filebeat +offset: 99362887 +length: 100 +snippetSha256: e81df9ab08f801188a6912a2c3651233049a50474063deba3181be22d50e4c14 +fileSha256: e29c151f3eae6d2aa6c57ce6810a1ca657fd29cd335d85c83d3b76fddaf0d7ed + +### byte snippet to follow ### +.5.4.82.5.4.92006-015m.rate8.11.2-9765625: type ::ffff::method:scheme:status<> Date: Mon, 22 Jun 2026 13:16:33 -0400 Subject: [PATCH 017/168] feat: support envoy bin classifier Signed-off-by: Rez Moss --- .../binary/classifier_cataloger_test.go | 121 ++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 14 ++ .../snippets/envoy/1.22.10/linux-amd64/envoy | Bin 0 -> 449 bytes .../snippets/envoy/1.23.7/linux-amd64/envoy | Bin 0 -> 569 bytes .../snippets/envoy/1.24.5/linux-amd64/envoy | Bin 0 -> 749 bytes .../snippets/envoy/1.24.9/linux-amd64/envoy | Bin 0 -> 349 bytes .../snippets/envoy/1.30.1/linux-amd64/envoy | Bin 0 -> 329 bytes .../snippets/envoy/1.30.2/linux-amd64/envoy | Bin 0 -> 429 bytes .../snippets/envoy/1.30.6/linux-amd64/envoy | Bin 0 -> 429 bytes .../snippets/envoy/1.31.2/linux-amd64/envoy | Bin 0 -> 429 bytes .../snippets/envoy/1.34.12/linux-amd64/envoy | Bin 0 -> 430 bytes .../snippets/envoy/1.35.4/linux-amd64/envoy | Bin 0 -> 430 bytes .../snippets/envoy/1.36.1/linux-amd64/envoy | Bin 0 -> 849 bytes .../pkg/cataloger/binary/testdata/config.yaml | 78 +++++++++++ 14 files changed, 213 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.22.10/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.23.7/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.24.5/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.24.9/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.1/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.2/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.6/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.31.2/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.34.12/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.35.4/linux-amd64/envoy create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.36.1/linux-amd64/envoy diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index a64ea072b..a57290a9b 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -2778,6 +2778,127 @@ func Test_Cataloger_PositiveCases(t *testing.T) { }, }, }, + { + logicalFixture: "envoy/1.22.10/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.22.10", + Type: "binary", + PURL: "pkg:generic/envoy@1.22.10", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.24.5/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.24.5", + Type: "binary", + PURL: "pkg:generic/envoy@1.24.5", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.30.2/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.30.2", + Type: "binary", + PURL: "pkg:generic/envoy@1.30.2", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.31.2/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.31.2", + Type: "binary", + PURL: "pkg:generic/envoy@1.31.2", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.34.12/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.34.12", + Type: "binary", + PURL: "pkg:generic/envoy@1.34.12", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.35.4/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.35.4", + Type: "binary", + PURL: "pkg:generic/envoy@1.35.4", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.23.7/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.23.7", + Type: "binary", + PURL: "pkg:generic/envoy@1.23.7", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.24.9/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.24.9", + Type: "binary", + PURL: "pkg:generic/envoy@1.24.9", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.30.1/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.30.1", + Type: "binary", + PURL: "pkg:generic/envoy@1.30.1", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.30.6/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.30.6", + Type: "binary", + PURL: "pkg:generic/envoy@1.30.6", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, + { + logicalFixture: "envoy/1.36.1/linux-amd64", + expected: pkg.Package{ + Name: "envoy", + Version: "1.36.1", + Type: "binary", + PURL: "pkg:generic/envoy@1.36.1", + Locations: locations("envoy"), + Metadata: metadata("envoy-binary"), + }, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 56f7865de..c5229235b 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -1076,6 +1076,20 @@ func DefaultClassifiers() []binutils.Classifier { m.FileContentsVersionMatcher(`(?s)\[source/.{0,200}\x00(?P1\.1[0-9]\.[0-9]+(-dev)?)\x00`), // 1.x [NUL]1.6.0[NUL]RELEASE m.FileContentsVersionMatcher(`(?s)\x00(?P1\.[0-9]\.[0-9]+(-dev)?)\x00.{0,20}RELEASE`), + // contrib 1.3x [NUL]1.34.12[NUL]envoy_quiche... or [NUL]1.30.2[NUL]envoy/extensions... or [NUL]1.30.8[NUL]envoy:// + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.3[0-9]\.[0-9]+(-dev)?)\x00envoy[/._:]`), + // contrib 1.3x [NUL]1.30.7[NUL]...envoy.service or [NUL]1.30.6[NUL]...envoy.data + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.3[0-9]\.[0-9]+(-dev)?)\x00.{0,200}envoy[./]`), + // contrib1.3x [NUL]1.31.2[NUL]...envoy_quic or [NUL]1.36.x[NUL]...envoy_quic + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.3[0-9]\.[0-9]+(-dev)?)\x00.{0,1000}envoy_quic`), + // contrib 1.3x [NUL]1.30.1[NUL]TLS client + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.3[0-9]\.[0-9]+(-dev)?)\x00TLS `), + // contrib 1.2x [NUL]1.24.9[NUL]envoy/extensions... + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.2[0-9]\.[0-9]+(-dev)?)\x00envoy[/._]`), + // contrib 1.2x [NUL]1.22.10[NUL]...envoy. or [NUL]1.24.5[NUL]...envoy. + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.2[0-9]\.[0-9]+(-dev)?)\x00.{0,500}envoy\.`), + // contrib 1.2x [NUL]1.23.7[NUL]...envoy_internal + m.FileContentsVersionMatcher(`(?s)\x00(?P1\.2[0-9]\.[0-9]+(-dev)?)\x00.{0,300}envoy_`), ), Package: "envoy", PURL: mustPURL("pkg:generic/envoy@version"), diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.22.10/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.22.10/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..ebf4ad28626892656432ed1d3aeea559e070107a GIT binary patch literal 449 zcmZ8eO^eh(6wEolqG2vGilMuc{>nvABRinWFdMzNvh>I6Odv_e?%vTw{O^Wky?A?6 z6-8COMbFS8Sgzrz4BjWmEs|I?n5%MvWyqHnQHsm7jOz;d?xHEpDw>pZ7nf7q~y zylc_#KLx2Mtq3mm-|pS_*N6M+|6~7VU%fgU-WJ{0?zVq8oxAtPyY8Vse(ui2@pR>A z3mqXPNJvLOOUNiau4{-H9107{mocGP+sn8>aT`8`Wq|krhNvC5n~-wRk2Cb);}}!^ svZfqC&*xw7wjRavLn^1UuJkrrN>doBYhB&+{pk!p9wFsZ3@gO^7pF6d+yDRo literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.23.7/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.23.7/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..e64339de8b2bf89849e41371236ee5cfc71e601b GIT binary patch literal 569 zcmZvaO>5&Y5QcsBuNVk{_L9hwB|GkEVfV1GUy$B|tQo}uSu&Oq(|^B8w|nbJ1JCfz z^MZn&X#fiM_*O@26XgLq);6sYwWn~+(*UAnbqa1-D4!?Ir5d1bnCwrSm^jIXnZ_8& zNXcFywOWwu87kf>*&6D2FFVaurv&Q-avBAr+B#4F!}vHNH)tCxRAVJtEBjWRETggG zBR3qC7c`t z&Ms8Vfr~>|rFdHn9Zj6)7>?u3T3SDzi<=jJFsAwyGw4Sc;Un0T8LpIn-m)G&ebrfm zvyi+q1TKM_=YOR7wiE~0r>8wWLkwUM_6Sy&?s$OT9)a!@a)OOkD(D4P(ggR#)nMMvRZ?Gg5v3b8kT-u9Olb(B(uZP10>y;+jjBVw9&MJZm$#K-K#% exVVsM4cb?)Kh9vhD}wJ@7ei}y))}4YveI8xbhLf| literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.24.5/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.24.5/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..1f22478b929d83aee614578e6a6aedb726b47f39 GIT binary patch literal 749 zcmZ8fOOKl{5a!IU7%7U{N?XLs;N;rJsZtN^9gXcVv6k2fOg8k_cbwfy)rtdnX8gSd zk5@Va`CHiYVC_or481(by6W~ym8SnsNFnk4Sof*krs#-+TnsX0!8G3 z*?{R@9Lr{N3e}!;!An@4i|h;f51hya{rEp-GO18MudG_rdr0S*87KFLOu@0OZX6V@ zG)zH1KYTBwiPJN{Esq=*`%#?NLzNGZrjrBf#%&>!{rH|Fq~)-Fy3fQPCVTv;g`T#R e%fn-nucd47V;D3qod?qBhO4@;z-6;ljimfrQLM>!Q=gzuF@OH zxAJI;>MFH2UFW-au+1d-qn>*McHgXdoM)-WGueahP4o&DZ3^f(F!+HfYKP!bP!E#2 zAvp>l*r7y=grdZI-2F=d(+fnv`kaEn(kaYOTCxn(ynE=NIi|Y%Azz#dvh&6D?X=s`fs~=DYb0mCb^zirrx~F=2iI{QPgjV literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.1/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.1/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..597d46fb92ccb012c6719f362c97951993328d1d GIT binary patch literal 329 zcmXw#&1%Ci5QKf!Q!M0INVS$FTfXHGN};6C7CZ(l>(+rZ3x43piJ zdZ7HZ+{MDYQt82>L8&`2Q@)g`2b5B*Io)nl&XcRP*$OUt9}OWnZh~X-ZD7N;Riu%0 z!r^lrQVzE5{fzo8{? zi(fNuZA5B`;!Lu#$tJi{I=6&TnngFxGt%*_SuI+k>!E74GODtsww!Y`s%IrMtC$i- zZ*4{2l!jJyPq}8g=8dhDk+McrtNv0^>oJ~(z-zcUbq^$>NtEHf{ zw9Hi7F0X{>CFdoWlv%dl?@7NVkiRoY^2GXS@;9i%c>9DMS-r;SfRZ^*AUj|j_X wkPl-tenD9Ch35xMNgp1Q7rb8)QyviK4M*p^GuXX8AIY6YAOU*l%rwHKZ#+Yc*Z=?k literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.6/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.30.6/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..c69ef23797498d8d23540ad44ea0e8bfaa8e6178 GIT binary patch literal 429 zcmXw$&2GaW5QKg9Qy}HiTpSD-6RYi|s?+Z~_(M+;O z92llczSY_Lf;2GGx2;yDj+l0|ADH00E-9Q&NN;;9Iz6z-Oel@w+W1~MK8b#dN?|7v zx#dPG*`b~UhvnQ!qYZ#WtGSsR2oIw6A>uzwwVmI#s&~+e-Z{mcK}6A+#joYoYumTN z*oD$-*()$!pcM{%C$>u*SJ$i6icL2{_7{^;W+^(~@%SC-eivN?JLlij+D^(op*E6~%>K3d_D`b{Mw68IJ_Z zy*|g9(iPj0SUMTa#F(+n?Q|rZ((ziB%LUu(3Amb$MAHzgQsB`_5rR(Km|!F52tp>6 zSoDlWrKOkF^wBb8Ng6~ssm|47S~C8MMMJeTfLg^U6BX3@qzqamnV^Y7Pl@+FM8Tj# zQ7g`rFqB)PMViKOx7!i_Y)Cenq*an?&hG-*pZE_JBxD_Gdx({J^fl_KCk9FxD?vQUthM%8qBAuEm eMDSfUWte}~&ij?SZeRrm;SHmEEG5>*|CitU5{&); literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.34.12/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.34.12/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..45b46380a86de014f51514f2d9519007978e5963 GIT binary patch literal 430 zcma)1OKJl#49(i7Fl1YTf8)kkKS9-5l4>8 zwcc1IYS>3Eg|tq(z-CudE z&XBjcB$cu&k_2Ph?Uv_TNB$d=_sXfxbA9J`h;ah`%0Itec)y+Sv2Nr}o1IjfNB4NX zOyz)a#knR(dB#X{+CR>*)TQBiO?|pz%$K$G_JoS2kJzUp*2`1$6-!+q_S)i4k$(^F Lp$aZNww}HL{hN>` literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.35.4/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.35.4/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..f950eb6a734350bb0d523eb43d15ea7f28178368 GIT binary patch literal 430 zcmX|+%}&EG5QKBiQ&{R175~I`5{Xmc07pRL7}@J}Yss|g zYOq{rYI(z2L4^V4I!lo|&fB`)Y&OJR3X)q*QZn&5#{5ZEsV3f$2lD-MN6O`h*R^6> zA-7F+eSJ7B0S-7!7_)K4MjU)J`zsz?n6KoF(;RXdPP9?gyx1|6T}}z1yy9uWx!mB# z6bk;Myi9=ikjF8n`;gZemMNtD5XNKN0#tSh(Y?$$J!V)&Or_dohw#1@t!^pL?&+&w Gn!_()Cya3b literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.36.1/linux-amd64/envoy b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/envoy/1.36.1/linux-amd64/envoy new file mode 100644 index 0000000000000000000000000000000000000000..f35b7f961cf5c20023739c82c369f8cc39455caf GIT binary patch literal 849 zcmYjQ!EW0y4AnVbL7laljpt`j*+l0>D`v}nsR)rCZ!XR>V*ob$BP*rshd!}*lYCfZp1*DO!7R&@!N z1yAH0lj$sJHH9P=dEU*FX;$bNFF5tc%ABi=r#w?sROm8CRZJ#VS69%UBElO@h#s`J z*1rHjlLK}U6|5dX%ih=%{QkrEO`6|uhEBE?VL+*_K`Xc&u})gs%5ER3YVAJZ*0?W( z;}AZCNSVDb>XxnDlRHpS{Tk5)>6+w{2)575ZGso_cF@mK(4Qx z_H0QM7v9Ce+j`7n%gxIbP@2iLsWt8(wcm-qlIj-!){NzK|@m&oi-pX20aP!^JuyqiUF;cYTOi|t=yi& SqO*oN3WZu8Z4{SH5`O_->l{S@ literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml index 53fb35c64..41d402bc9 100644 --- a/syft/pkg/cataloger/binary/testdata/config.yaml +++ b/syft/pkg/cataloger/binary/testdata/config.yaml @@ -1737,3 +1737,81 @@ from-images: platform: linux/amd64 paths: - /usr/share/elastic-agent/data/elastic-agent-1c21b0/elastic-agent + + # contrib builds + - version: 1.22.10 + images: + - ref: envoyproxy/envoy:contrib-v1.22.10@sha256:2c479dd69aaaf555ab48ab9142b5970b74ee472e50f08d2b85caa1460c19f689 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.24.5 + images: + - ref: envoyproxy/envoy:contrib-v1.24.5@sha256:9a32eaff6798dd62a7d6662a02226d47df694406d1698f000ad25dd1a3355952 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.30.2 + images: + - ref: envoyproxy/envoy:contrib-v1.30.2@sha256:8878516ce3c8f099300243fadacb2e3d869d447dfd276ac2661c6c06c92bcd45 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.31.2 + images: + - ref: envoyproxy/envoy:contrib-v1.31.2@sha256:ebdf98ff3b26d5fa70276fb43f0dbca168f49aa5aa1fe5f18382d391f482aa3e + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.34.12 + images: + - ref: envoyproxy/envoy:contrib-v1.34.12@sha256:50da8a5221c9afc84df79f384aa9ee9c7000f8509d3f2562a58d84cb2fd39645 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.23.7 + images: + - ref: envoyproxy/envoy:contrib-v1.23.7@sha256:4589b08900e41de763d19ac3c854c280796a575f58c218c5e8b2f2a6e4765dda + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.24.9 + images: + - ref: envoyproxy/envoy:contrib-v1.24.9@sha256:e6cdba2e747f2c024821cec58d2e2833cd0e6512c61d1dbe7200f8046235a026 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.30.1 + images: + - ref: envoyproxy/envoy:contrib-v1.30.1@sha256:92d50d25c28d68979a20607736e401e4c05c717109fc0d07d688aaa3db35bd94 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.30.6 + images: + - ref: envoyproxy/envoy:contrib-v1.30.6@sha256:58bd1cb68eea4ffd5b4d41112bc31299057f17e72c6cacaf7431fd0ee6b5222b + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.35.4 + images: + - ref: envoyproxy/envoy:contrib-v1.35.4@sha256:07378820f9669a3c7eac5d0a88fda78ccb8dfdd4100388db1110b2038f8d5d21 + platform: linux/amd64 + paths: + - /usr/local/bin/envoy + + - version: 1.36.1 + images: + - ref: envoyproxy/envoy:contrib-v1.36.1@sha256:f2001f6f72abc22e7ac9cebe940e48570ee5970703ada61bca6919c687b2accf + platform: linux/amd64 + paths: + - /usr/local/bin/envoy From 506ad5d6a726028536c7b07f0c344106aedfd77a Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 22 Jun 2026 14:04:48 -0400 Subject: [PATCH 018/168] refactor release pipeline: TAG_TOKEN, skip-checks gate, dependabot/zizmor cleanup (#5003) Signed-off-by: Alex Goodman --- .github/dependabot.yml | 25 ------------------------- .github/workflows/release.yaml | 20 ++++++++++++++++---- .github/zizmor.yml | 5 ----- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c7097b2b8..d52390045 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,4 @@ # Dependabot configuration -# -# Grouping behavior (see inline comments for details): -# - Minor + patch updates: grouped into a single PR per ecosystem -# - Major version bumps: individual PR per dependency -# - Security updates: individual PR per dependency -# -# Note: "patch" refers to semver version bumps (1.2.3 -> 1.2.4), not security fixes. -# Security updates are identified separately via GitHub's Advisory Database and -# can be any version bump (patch, minor, or major) that fixes a known CVE. version: 2 @@ -30,14 +21,6 @@ updates: - dependency-name: "github.com/aquasecurity/go-version" - dependency-name: "github.com/knqyf263/go-apk-version" - dependency-name: "github.com/knqyf263/go-deb-version" - groups: - go-minor-patch: - applies-to: version-updates # security updates get individual PRs - patterns: - - "*" - update-types: # major omitted, gets individual PRs - - "minor" - - "patch" - package-ecosystem: "github-actions" directories: @@ -51,11 +34,3 @@ updates: open-pull-requests-limit: 10 labels: - "dependencies" - groups: - actions-minor-patch: - applies-to: version-updates # security updates get individual PRs - patterns: - - "*" - update-types: # major omitted, gets individual PRs - - "minor" - - "patch" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f3387ce97..b282397d4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,6 +21,11 @@ on: options: - "all" - "install-script-only" + skip-checks: + description: skip the check-gate (release even if checks haven't passed on main) + type: boolean + default: false + required: false jobs: version-available: @@ -32,7 +37,7 @@ jobs: version: ${{ github.event.inputs.version }} check-gate: - if: ${{ github.event.inputs.phase == 'all' }} + if: ${{ github.event.inputs.phase == 'all' && !inputs.skip-checks }} permissions: contents: read checks: read # required for getting the status of specific check names @@ -45,7 +50,14 @@ jobs: release: needs: [check-gate, version-available] - if: ${{ github.event.inputs.phase == 'all' }} + # run even when check-gate is skipped, but never when version-available + # failed/was skipped, nor when check-gate failed or was cancelled. note: + # always() disables the implicit success() gate on ALL needs, so the + # version-available requirement must be re-asserted explicitly here. + if: >- + ${{ always() + && needs.version-available.result == 'success' + && !contains(fromJSON('["failure", "cancelled"]'), needs.check-gate.result) }} environment: release # runs-on.com: compute instances for parallel builds # spot disabled: reliability for build workflows (used for releases too) @@ -82,8 +94,8 @@ jobs: - name: Build & publish release artifacts run: make ci-release env: - # used for pushing tags - DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + # for pushing tags (does not inherit workflow permissions) + TAG_TOKEN: ${{ secrets.TAG_TOKEN }} RELEASE_VERSION: ${{ github.event.inputs.version }} # for mac signing and notarization... QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 9002617dd..ee5ca02ba 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,9 +1,4 @@ rules: - unpinned-uses: - config: - policies: - # anchore/workflows is an internal repository; using @main is acceptable - anchore/*: any dangerous-triggers: ignore: # Safe use of pull_request_target - only runs trusted scripts from base repo, From e9af7d218c359fed6dc20360e8dc969cb588b9f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:18:54 +0000 Subject: [PATCH 019/168] chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.7.10 to 6.8.1 (#5006) Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.7.10 to 6.8.1. - [Release notes](https://github.com/jedib0t/go-pretty/releases) - [Commits](https://github.com/jedib0t/go-pretty/compare/v6.7.10...v6.8.1) --- updated-dependencies: - dependency-name: github.com/jedib0t/go-pretty/v6 dependency-version: 6.8.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 563220d46..9b19a2fb1 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/iancoleman/strcase v0.3.0 github.com/invopop/jsonschema v0.14.0 - github.com/jedib0t/go-pretty/v6 v6.7.10 + github.com/jedib0t/go-pretty/v6 v6.8.1 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 github.com/klauspost/compress v1.18.6 diff --git a/go.sum b/go.sum index 031a56fc3..433327569 100644 --- a/go.sum +++ b/go.sum @@ -624,8 +624,8 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.7.10 h1:B/2qW2Bkv2L6n14PP8o1kx75kWzHOQ3YTluWzg9icac= -github.com/jedib0t/go-pretty/v6 v6.7.10/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= +github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= From f827f91ec111d8c56def6edd088d0c07b9cdf719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:19:47 +0000 Subject: [PATCH 020/168] chore(deps): bump golang.org/x/mod from 0.36.0 to 0.37.0 (#5007) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.36.0 to 0.37.0. - [Commits](https://github.com/golang/mod/compare/v0.36.0...v0.37.0) --- updated-dependencies: - dependency-name: golang.org/x/mod dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9b19a2fb1..a63a0437a 100644 --- a/go.mod +++ b/go.mod @@ -93,7 +93,7 @@ require ( go.uber.org/goleak v1.3.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f - golang.org/x/mod v0.36.0 + golang.org/x/mod v0.37.0 golang.org/x/net v0.55.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.45.0 diff --git a/go.sum b/go.sum index 433327569..1f9983355 100644 --- a/go.sum +++ b/go.sum @@ -1080,8 +1080,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= From 684c7018beebff8b35b306306a347cdba0438e0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:34:47 +0000 Subject: [PATCH 021/168] chore(deps): bump golang.org/x/net from 0.55.0 to 0.56.0 (#5004) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0. - [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a63a0437a..93f92c004 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/mod v0.37.0 - golang.org/x/net v0.55.0 + golang.org/x/net v0.56.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.45.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 1f9983355..d5fb8e633 100644 --- a/go.sum +++ b/go.sum @@ -1126,8 +1126,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From 5eefd73ac7e0003551a83b2fedf045b3e200a3a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:50:24 +0000 Subject: [PATCH 022/168] chore(deps): bump golang.org/x/tools from 0.45.0 to 0.46.0 (#5008) Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.45.0 to 0.46.0. - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.45.0...v0.46.0) --- updated-dependencies: - dependency-name: golang.org/x/tools dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 93f92c004..6e473cbf3 100644 --- a/go.mod +++ b/go.mod @@ -96,7 +96,7 @@ require ( golang.org/x/mod v0.37.0 golang.org/x/net v0.56.0 golang.org/x/time v0.15.0 - golang.org/x/tools v0.45.0 + golang.org/x/tools v0.46.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.51.0 ) diff --git a/go.sum b/go.sum index d5fb8e633..7dc6eff9c 100644 --- a/go.sum +++ b/go.sum @@ -1314,8 +1314,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From fea4a501246d151a05191a7dd92b556ee84489c4 Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Tue, 23 Jun 2026 10:58:22 -0400 Subject: [PATCH 023/168] feat: deno cataloger #4417 (#4523) Signed-off-by: Rez Moss Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- internal/constants.go | 3 +- internal/packagemetadata/generated.go | 2 + internal/packagemetadata/names.go | 2 + internal/task/package_tasks.go | 2 +- schema/json/schema-16.1.5.json | 4351 +++++++++++++++++ schema/json/schema-latest.json | 47 +- .../helpers/originator_supplier_test.go | 2 + .../cataloger/javascript/capabilities.yaml | 39 + syft/pkg/cataloger/javascript/cataloger.go | 4 +- .../cataloger/javascript/cataloger_test.go | 1 + .../cataloger/javascript/parse_deno_lock.go | 296 ++ .../javascript/parse_deno_lock_test.go | 83 + .../test-fixtures/deno/corrupt/deno.lock | 4 + .../javascript/test-fixtures/deno/deno.lock | 27 + .../testdata/glob-paths/src/deno.lock | 3 + syft/pkg/deno.go | 19 + 16 files changed, 4881 insertions(+), 4 deletions(-) create mode 100644 schema/json/schema-16.1.5.json create mode 100644 syft/pkg/cataloger/javascript/parse_deno_lock.go create mode 100644 syft/pkg/cataloger/javascript/parse_deno_lock_test.go create mode 100644 syft/pkg/cataloger/javascript/test-fixtures/deno/corrupt/deno.lock create mode 100644 syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock create mode 100644 syft/pkg/cataloger/javascript/testdata/glob-paths/src/deno.lock create mode 100644 syft/pkg/deno.go diff --git a/internal/constants.go b/internal/constants.go index c00065718..bff8cc31e 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,7 +3,7 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.1.4" + JSONSchemaVersion = "16.1.5" // Changelog // 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). @@ -11,5 +11,6 @@ const ( // 16.1.2 - placeholder for 16.1.2 changelog // 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata // 16.1.4 - add BunLockEntry metadata type for bun.lock support + // 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support ) diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go index 905670c55..eaa9643d3 100644 --- a/internal/packagemetadata/generated.go +++ b/internal/packagemetadata/generated.go @@ -20,6 +20,8 @@ func AllTypes() []any { pkg.CondaMetaPackage{}, pkg.DartPubspec{}, pkg.DartPubspecLockEntry{}, + pkg.DenoLockEntry{}, + pkg.DenoRemoteLockEntry{}, pkg.DotnetDepsEntry{}, pkg.DotnetPackagesLockEntry{}, pkg.DotnetPortableExecutableEntry{}, diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go index 7c9b76cd9..3e8d9f174 100644 --- a/internal/packagemetadata/names.go +++ b/internal/packagemetadata/names.go @@ -73,6 +73,8 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"), jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"), jsonNames(pkg.DartPubspec{}, "dart-pubspec"), + jsonNames(pkg.DenoLockEntry{}, "deno-lock-entry"), + jsonNames(pkg.DenoRemoteLockEntry{}, "deno-remote-lock-entry"), jsonNames(pkg.DotnetDepsEntry{}, "dotnet-deps-entry", "DotnetDepsMetadata"), jsonNames(pkg.DotnetPortableExecutableEntry{}, "dotnet-portable-executable-entry"), jsonNames(pkg.DpkgArchiveEntry{}, "dpkg-archive-entry"), diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index 3347c50dd..016042424 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -105,7 +105,7 @@ func DefaultPackageTaskFactories() Factories { func(cfg CatalogingFactoryConfig) pkg.Cataloger { return javascript.NewLockCataloger(cfg.PackagesConfig.JavaScript) }, - pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, JavaScript, Node, NPM, + pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, JavaScript, Node, NPM, "deno", ), newSimplePackageTaskFactory(php.NewComposerLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "php", "composer"), newSimplePackageTaskFactory(php.NewPearCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, pkgcataloging.ImageTag, "php", "pear"), diff --git a/schema/json/schema-16.1.5.json b/schema/json/schema-16.1.5.json new file mode 100644 index 000000000..88cec5ca3 --- /dev/null +++ b/schema/json/schema-16.1.5.json @@ -0,0 +1,4351 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.1.5/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object", + "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman." + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string", + "description": "Value is the CPE string identifier." + }, + "source": { + "type": "string", + "description": "Source is the source where this CPE was obtained or generated from." + } + }, + "type": "object", + "required": [ + "cpe" + ], + "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases." + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, + "Descriptor": { + "properties": { + "name": { + "type": "string", + "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")." + }, + "version": { + "type": "string", + "description": "Version is the version of the tool that generated this SBOM." + }, + "configuration": { + "description": "Configuration contains the tool configuration used during SBOM generation." + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation." + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array", + "description": "Artifacts is the list of packages discovered and placed into the catalog" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array", + "description": "note: must have omitempty" + }, + "source": { + "$ref": "#/$defs/Source", + "description": "Source represents the original object that was cataloged" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease", + "description": "Distro represents the Linux distribution that was detected from the source" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor", + "description": "Descriptor is a block containing self-describing information about syft" + }, + "schema": { + "$ref": "#/$defs/Schema", + "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "type": { + "type": "string", + "description": "Type is type of entry could be package or project for internal refs" + }, + "executables": { + "additionalProperties": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing." + }, + "appCpe": { + "type": "string", + "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for this file within the SBOM." + }, + "location": { + "$ref": "#/$defs/Coordinates", + "description": "Location is the file path and layer information where this file was found." + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry", + "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type." + }, + "contents": { + "type": "string", + "description": "Contents is the file contents for small files." + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains cryptographic hashes of the file contents." + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array", + "description": "Licenses contains license information discovered within this file." + }, + "executable": { + "$ref": "#/$defs/Executable", + "description": "Executable contains executable metadata if this file is a binary." + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Unknowns contains unknown fields for forward compatibility." + } + }, + "type": "object", + "required": [ + "id", + "location" + ], + "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages." + }, + "FileLicense": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or text as found in the file." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence", + "description": "Evidence contains supporting evidence for this license detection." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ], + "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression." + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer", + "description": "Confidence is the confidence score for this license detection (0-100)." + }, + "offset": { + "type": "integer", + "description": "Offset is the byte offset where the license text starts in the file." + }, + "extent": { + "type": "integer", + "description": "Extent is the length of the license text in bytes." + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ], + "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level." + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer", + "description": "Mode is the Unix file permission mode in octal format." + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")." + }, + "linkDestination": { + "type": "string", + "description": "LinkDestination is the target path for symbolic links." + }, + "userID": { + "type": "integer", + "description": "UserID is the file owner user ID." + }, + "groupID": { + "type": "integer", + "description": "GroupID is the file owner group ID." + }, + "mimeType": { + "type": "string", + "description": "MIMEType is the MIME type of the file contents." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ], + "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file." + }, + "GgufFileHeader": { + "properties": { + "ggufVersion": { + "type": "integer", + "description": "GGUFVersion is the GGUF format version (e.g., 3)" + }, + "fileSize": { + "type": "integer", + "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")" + }, + "quantization": { + "type": "string", + "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")" + }, + "parameters": { + "type": "integer", + "description": "Parameters is the number of model parameters (if present in header)" + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensors in the model" + }, + "header": { + "type": "object", + "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication." + }, + "metadataHash": { + "type": "string", + "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames." + }, + "parts": { + "items": { + "$ref": "#/$defs/GgufFileHeader" + }, + "type": "array", + "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)." + } + }, + "type": "object", + "required": [ + "ggufVersion", + "tensorCount" + ], + "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file." + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field." + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptBunLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of runtime dependencies and their version specifiers" + }, + "optionalDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "OptionalDependencies is a map of optional dependencies and their version specifiers" + }, + "peerDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "PeerDependencies is a map of peer dependencies and their version specifiers" + }, + "bin": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Bin is a map of binary names to the paths they are installed to" + }, + "os": { + "type": "string", + "description": "OS is the operating system constraint for the package (e.g. \"darwin\")" + }, + "cpu": { + "type": "string", + "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies", + "optionalDependencies", + "peerDependencies", + "bin", + "os", + "cpu" + ], + "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\"" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptPnpmLockEntry": { + "properties": { + "resolution": { + "$ref": "#/$defs/PnpmLockResolution", + "description": "Resolution is the resolution information for the package" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolution", + "dependencies" + ], + "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or expression as found." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URLs are URLs where license text or information can be found." + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array", + "description": "Locations are file locations where this license was discovered." + }, + "contents": { + "type": "string", + "description": "Contents is the full license text content." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ], + "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations." + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string", + "description": "PrettyName is a human-readable operating system name with version." + }, + "name": { + "type": "string", + "description": "Name is the operating system name without version information." + }, + "id": { + "type": "string", + "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")." + }, + "idLike": { + "$ref": "#/$defs/IDLikes", + "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from." + }, + "version": { + "type": "string", + "description": "Version is the operating system version including codename if available." + }, + "versionID": { + "type": "string", + "description": "VersionID is the operating system version number or identifier." + }, + "versionCodename": { + "type": "string", + "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")." + }, + "buildID": { + "type": "string", + "description": "BuildID is a build identifier for the operating system." + }, + "imageID": { + "type": "string", + "description": "ImageID is an identifier for container or cloud images." + }, + "imageVersion": { + "type": "string", + "description": "ImageVersion is the version for container or cloud images." + }, + "variant": { + "type": "string", + "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")." + }, + "variantID": { + "type": "string", + "description": "VariantID is the lower-case operating system variant identifier." + }, + "homeURL": { + "type": "string", + "description": "HomeURL is the homepage URL for the operating system." + }, + "supportURL": { + "type": "string", + "description": "SupportURL is the support or help URL for the operating system." + }, + "bugReportURL": { + "type": "string", + "description": "BugReportURL is the bug reporting URL for the operating system." + }, + "privacyPolicyURL": { + "type": "string", + "description": "PrivacyPolicyURL is the privacy policy URL for the operating system." + }, + "cpeName": { + "type": "string", + "description": "CPEName is the Common Platform Enumeration name for the operating system." + }, + "supportEnd": { + "type": "string", + "description": "SupportEnd is the end of support date or version identifier." + }, + "extendedSupport": { + "type": "boolean", + "description": "ExtendedSupport indicates whether extended security or support is available." + } + }, + "type": "object", + "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files." + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Arbitrary key-value pairs that can be used to annotate a location" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GgufFileHeader" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptBunLockEntry" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptPnpmLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PnpmLockResolution": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "integrity" + ], + "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmFileEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the file download URL" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest is the hash digest of the file hosted at the URL" + } + }, + "type": "object", + "required": [ + "url", + "digest" + ] + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests (for the base package without extras)" + }, + "marker": { + "type": "string", + "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment" + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications for the base package (without extras)" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPdmLockExtraVariant" + }, + "type": "array", + "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)" + } + }, + "type": "object", + "required": [ + "summary", + "files" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPdmLockExtraVariant": { + "properties": { + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependencies specific to this extras variant" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files specific to this variant (only populated if different from base)" + }, + "marker": { + "type": "string", + "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")" + } + }, + "type": "object", + "required": [ + "extras" + ], + "description": "PythonPdmLockExtraVariant represents a specific extras combination variant within a PDM lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string", + "description": "Parent is the ID of the parent artifact in this relationship." + }, + "child": { + "type": "string", + "description": "Child is the ID of the child artifact in this relationship." + }, + "type": { + "type": "string", + "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")." + }, + "metadata": { + "description": "Metadata contains additional relationship-specific metadata." + } + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ], + "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package." + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "Schema": { + "properties": { + "version": { + "type": "string", + "description": "Version is the JSON schema version for this document format." + }, + "url": { + "type": "string", + "description": "URL is the URL to the JSON schema definition document." + } + }, + "type": "object", + "required": [ + "version", + "url" + ], + "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format." + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for the analyzed source artifact." + }, + "name": { + "type": "string", + "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)." + }, + "version": { + "type": "string", + "description": "Version is the version of the analyzed artifact (e.g., image tag)." + }, + "supplier": { + "type": "string", + "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance." + }, + "type": { + "type": "string", + "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")." + }, + "metadata": { + "description": "Metadata contains additional source-specific metadata." + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 4589abee2..88cec5ca3 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.1.4/document", + "$id": "anchore.io/schema/syft/json/16.1.5/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -748,6 +748,45 @@ ], "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, "Descriptor": { "properties": { "name": { @@ -2658,6 +2697,12 @@ { "$ref": "#/$defs/DartPubspecLockEntry" }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, { "$ref": "#/$defs/DotnetDepsEntry" }, diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index 388f6c274..f42654945 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -57,6 +57,8 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.YarnLockEntry{}, pkg.TerraformLockProviderEntry{}, pkg.GGUFFileHeader{}, + pkg.DenoLockEntry{}, + pkg.DenoRemoteLockEntry{}, ) tests := []struct { name string diff --git a/syft/pkg/cataloger/javascript/capabilities.yaml b/syft/pkg/cataloger/javascript/capabilities.yaml index 0cec8d141..a83274b34 100644 --- a/syft/pkg/cataloger/javascript/capabilities.yaml +++ b/syft/pkg/cataloger/javascript/capabilities.yaml @@ -22,6 +22,7 @@ catalogers: config: javascript.CatalogerConfig # AUTO-GENERATED selectors: # AUTO-GENERATED - declared + - deno - directory - javascript - language @@ -29,6 +30,44 @@ catalogers: - npm - package parsers: # AUTO-GENERATED structure + - function: parseDenoLock + detector: # AUTO-GENERATED + method: glob # AUTO-GENERATED + criteria: # AUTO-GENERATED + - '**/deno.lock' + metadata_types: # AUTO-GENERATED + - pkg.DenoLockEntry + - pkg.DenoRemoteLockEntry + - pkg.NpmPackageLockEntry + package_types: # AUTO-GENERATED + - npm + purl_types: # AUTO-GENERATED + - npm + json_schema_types: # AUTO-GENERATED + - DenoLockEntry + - DenoRemoteLockEntry + - JavascriptNpmPackageLockEntry + capabilities: # MANUAL - preserved across regeneration + - name: license + default: false + - name: dependency.depth + default: + - direct + - indirect + - name: dependency.edges + default: "" + - name: dependency.kinds + default: + - runtime + - name: package_manager.files.listing + default: false + - name: package_manager.files.digests + default: false + - name: package_manager.package_integrity_hash + default: true + evidence: + - DenoLockEntry.Integrity + - DenoRemoteLockEntry.Integrity - function: parseBunLock detector: # AUTO-GENERATED method: glob # AUTO-GENERATED diff --git a/syft/pkg/cataloger/javascript/cataloger.go b/syft/pkg/cataloger/javascript/cataloger.go index 8cb98c105..d818a00aa 100644 --- a/syft/pkg/cataloger/javascript/cataloger.go +++ b/syft/pkg/cataloger/javascript/cataloger.go @@ -20,9 +20,11 @@ func NewLockCataloger(cfg CatalogerConfig) pkg.Cataloger { packageLockAdapter := newGenericPackageLockAdapter(cfg) pnpmLockAdapter := newGenericPnpmLockAdapter(cfg) bunLockAdapter := newGenericBunLockAdapter(cfg) + denoLockAdapter := newGenericDenoLockAdapter(cfg) return generic.NewCataloger("javascript-lock-cataloger"). WithParserByGlobs(packageLockAdapter.parsePackageLock, "**/package-lock.json"). WithParserByGlobs(yarnLockAdapter.parseYarnLock, "**/yarn.lock"). WithParserByGlobs(pnpmLockAdapter.parsePnpmLock, "**/pnpm-lock.yaml"). - WithParserByGlobs(bunLockAdapter.parseBunLock, "**/bun.lock") + WithParserByGlobs(bunLockAdapter.parseBunLock, "**/bun.lock"). + WithParserByGlobs(denoLockAdapter.parseDenoLock, "**/deno.lock") } diff --git a/syft/pkg/cataloger/javascript/cataloger_test.go b/syft/pkg/cataloger/javascript/cataloger_test.go index 04bd02e26..d615c793f 100644 --- a/syft/pkg/cataloger/javascript/cataloger_test.go +++ b/syft/pkg/cataloger/javascript/cataloger_test.go @@ -173,6 +173,7 @@ func Test_LockCataloger_Globs(t *testing.T) { name: "obtain package files", fixture: "testdata/glob-paths", expected: []string{ + "src/deno.lock", "src/package-lock.json", "src/pnpm-lock.yaml", "src/yarn.lock", diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock.go b/syft/pkg/cataloger/javascript/parse_deno_lock.go new file mode 100644 index 000000000..c11bc98f7 --- /dev/null +++ b/syft/pkg/cataloger/javascript/parse_deno_lock.go @@ -0,0 +1,296 @@ +package javascript + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/url" + "strings" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/unknown" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" +) + +type denoLock struct { + Version string `json:"version"` + Jsr map[string]denoJsrPackage `json:"jsr"` + Npm map[string]denoNpmPackage `json:"npm"` + Remote map[string]string `json:"remote"` +} + +type denoJsrPackage struct { + Integrity string `json:"integrity"` + Dependencies []string `json:"dependencies"` +} + +type denoNpmPackage struct { + Integrity string `json:"integrity"` + Dependencies []string `json:"dependencies"` +} + +type genericDenoLockAdapter struct { + cfg CatalogerConfig +} + +func newGenericDenoLockAdapter(cfg CatalogerConfig) genericDenoLockAdapter { + return genericDenoLockAdapter{ + cfg: cfg, + } +} + +func (a genericDenoLockAdapter) parseDenoLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + var pkgs []pkg.Package + dec := json.NewDecoder(reader) + + var lock denoLock + for { + if err := dec.Decode(&lock); errors.Is(err, io.EOF) { + break + } else if err != nil { + return nil, nil, fmt.Errorf("failed to parse deno.lock file: %w", err) + } + } + + for nameVersion, pkgMeta := range lock.Jsr { + name, version := parseDenoJsrNameVersion(nameVersion) + if name == "" || version == "" { + continue + } + pkgs = append(pkgs, newDenoJsrPackage(reader.Location, name, version, pkgMeta)) + } + + for nameVersion, pkgMeta := range lock.Npm { + name, version := parseDenoNpmNameVersion(nameVersion) + if name == "" || version == "" { + continue + } + pkgs = append(pkgs, newDenoNpmPackage(reader.Location, name, version, pkgMeta)) + } + + for rawURL, integrity := range lock.Remote { + name, version := parseDenoRemoteURL(rawURL) + if name == "" { + continue + } + pkgs = append(pkgs, newDenoRemotePackage(reader.Location, name, version, rawURL, integrity)) + } + + pkg.Sort(pkgs) + + return pkgs, dependency.Resolve(denoLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages") +} + +func parseDenoJsrNameVersion(nameVersion string) (name, version string) { + idx := strings.LastIndex(nameVersion, "@") + if idx <= 0 { + return "", "" + } + return nameVersion[:idx], nameVersion[idx+1:] +} + +func parseDenoNpmNameVersion(nameVersion string) (name, version string) { + if strings.HasPrefix(nameVersion, "@") { + rest := nameVersion[1:] + idx := strings.LastIndex(rest, "@") + if idx <= 0 { + return "", "" + } + return nameVersion[:idx+1], rest[idx+1:] + } + idx := strings.LastIndex(nameVersion, "@") + if idx <= 0 { + return "", "" + } + return nameVersion[:idx], nameVersion[idx+1:] +} + +func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package { + p := pkg.Package{ + Name: name, + Version: version, + Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + PURL: denoJsrPackageURL(name, version), + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.DenoLockEntry{ + Integrity: meta.Integrity, + Dependencies: meta.Dependencies, + }, + } + p.SetID() + return p +} + +func newDenoNpmPackage(location file.Location, name, version string, meta denoNpmPackage) pkg.Package { + p := pkg.Package{ + Name: name, + Version: version, + Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + PURL: denoNpmPackageURL(name, version), + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{ + Integrity: meta.Integrity, + }, + } + p.SetID() + return p +} + +func newDenoRemotePackage(location file.Location, name, version, rawURL, integrity string) pkg.Package { + p := pkg.Package{ + Name: name, + Version: version, + Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + PURL: denoRemotePackageURL(name, version, rawURL), + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.DenoRemoteLockEntry{ + URL: rawURL, + Integrity: integrity, + }, + } + p.SetID() + return p +} + +func parseDenoRemoteURL(rawURL string) (name, version string) { + rawURL = strings.TrimPrefix(rawURL, "https://") + rawURL = strings.TrimPrefix(rawURL, "http://") + + atIdx := strings.Index(rawURL, "@") + if atIdx == -1 { + slashIdx := strings.Index(rawURL, "/") + if slashIdx == -1 { + return rawURL, "" + } + return rawURL[:slashIdx], "" + } + + name = rawURL[:atIdx] + + rest := rawURL[atIdx+1:] + slashIdx := strings.Index(rest, "/") + if slashIdx == -1 { + version = rest + } else { + version = rest[:slashIdx] + } + + return name, version +} + +func extractRepositoryBase(rawURL string) string { + u, err := url.Parse(rawURL) + if err != nil { + return "" + } + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) +} + +func denoRemotePackageURL(name, version, rawURL string) string { + repositoryURL := extractRepositoryBase(rawURL) + var qualifiers packageurl.Qualifiers + if repositoryURL != "" { + qualifiers = packageurl.Qualifiers{{Key: "repository_url", Value: repositoryURL}} + } + + return packageurl.NewPackageURL( + packageurl.TypeNPM, + "", + name, + version, + qualifiers, + "", + ).ToString() +} + +func denoJsrPackageURL(name, version string) string { + var namespace string + fields := strings.SplitN(name, "/", 2) + if len(fields) > 1 { + namespace = fields[0] + name = fields[1] + } + + return packageurl.NewPackageURL( + packageurl.TypeNPM, + namespace, + name, + version, + packageurl.Qualifiers{{Key: "repository_url", Value: "https://jsr.io"}}, + "", + ).ToString() +} + +func denoNpmPackageURL(name, version string) string { + var namespace string + fields := strings.SplitN(name, "/", 2) + if len(fields) > 1 { + namespace = fields[0] + name = fields[1] + } + + return packageurl.NewPackageURL( + packageurl.TypeNPM, + namespace, + name, + version, + nil, + "", + ).ToString() +} + +func denoLockDependencySpecifier(p pkg.Package) dependency.Specification { + meta, ok := p.Metadata.(pkg.DenoLockEntry) + if !ok { + return dependency.Specification{} + } + + provides := []string{p.Name} + var requires []string + + for _, dep := range meta.Dependencies { + name := parseDenoDependencyName(dep) + if name != "" { + requires = append(requires, name) + } + } + + return dependency.Specification{ + ProvidesRequires: dependency.ProvidesRequires{ + Provides: provides, + Requires: requires, + }, + } +} + +func parseDenoDependencyName(dep string) string { + if strings.HasPrefix(dep, "jsr:") { + dep = strings.TrimPrefix(dep, "jsr:") + } else if strings.HasPrefix(dep, "npm:") { + dep = strings.TrimPrefix(dep, "npm:") + } + + if strings.HasPrefix(dep, "@") { + rest := dep[1:] + atIdx := strings.Index(rest, "@") + if atIdx > 0 { + return dep[:atIdx+1] + } + return dep + } + + idx := strings.Index(dep, "@") + if idx > 0 { + return dep[:idx] + } + return dep +} diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go new file mode 100644 index 000000000..2e70bc32a --- /dev/null +++ b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go @@ -0,0 +1,83 @@ +package javascript + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func TestParseDenoLock(t *testing.T) { + fixture := "test-fixtures/deno/deno.lock" + + expectedPkgs := []pkg.Package{ + { + Name: "@std/bytes", + Version: "1.0.2", + PURL: "pkg:npm/%40std/bytes@1.0.2?repository_url=https%3A%2F%2Fjsr.io", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.DenoLockEntry{ + Integrity: "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57", + }, + }, + { + Name: "@std/encoding", + Version: "1.0.5", + PURL: "pkg:npm/%40std/encoding@1.0.5?repository_url=https%3A%2F%2Fjsr.io", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.DenoLockEntry{ + Integrity: "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04", + Dependencies: []string{"jsr:@std/bytes@^1.0.0"}, + }, + }, + { + Name: "chalk", + Version: "5.3.0", + PURL: "pkg:npm/chalk@5.3.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{ + Integrity: "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + }, + }, + { + Name: "deno.land/std", + Version: "0.140.0", + PURL: "pkg:npm/deno.land%2Fstd@0.140.0?repository_url=https%3A%2F%2Fdeno.land", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.DenoRemoteLockEntry{ + URL: "https://deno.land/std@0.140.0/path/mod.ts", + Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + }, + }, + } + + for i := range expectedPkgs { + expectedPkgs[i].Locations.Add(file.NewLocation(fixture)) + } + + // @std/encoding depends => @std/bytes + expectedRelationships := []artifact.Relationship{ + { + From: expectedPkgs[0], // @std/bytes main + To: expectedPkgs[1], // @std/encoding dep + Type: artifact.DependencyOfRelationship, + }, + } + + adapter := newGenericDenoLockAdapter(DefaultCatalogerConfig()) + pkgtest.TestFileParser(t, fixture, adapter.parseDenoLock, expectedPkgs, expectedRelationships) +} + +func Test_corruptDenoLock(t *testing.T) { + adapter := newGenericDenoLockAdapter(DefaultCatalogerConfig()) + pkgtest.NewCatalogTester(). + FromFile(t, "test-fixtures/deno/corrupt/deno.lock"). + WithError(). + TestParser(t, adapter.parseDenoLock) +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/deno/corrupt/deno.lock b/syft/pkg/cataloger/javascript/test-fixtures/deno/corrupt/deno.lock new file mode 100644 index 000000000..fa09e6764 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/deno/corrupt/deno.lock @@ -0,0 +1,4 @@ +{ + "version": "4", + this is not valid json +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock new file mode 100644 index 000000000..c6ecf9efd --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock @@ -0,0 +1,27 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/bytes@^1.0.0": "1.0.2", + "jsr:@std/encoding@^1.0.0": "1.0.5", + "npm:chalk@5": "5.3.0" + }, + "jsr": { + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04", + "dependencies": [ + "jsr:@std/bytes@^1.0.0" + ] + } + }, + "npm": { + "chalk@5.3.0": { + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + } + }, + "remote": { + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d" + } +} diff --git a/syft/pkg/cataloger/javascript/testdata/glob-paths/src/deno.lock b/syft/pkg/cataloger/javascript/testdata/glob-paths/src/deno.lock new file mode 100644 index 000000000..a39b837e9 --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/glob-paths/src/deno.lock @@ -0,0 +1,3 @@ +{ + "version": "4" +} diff --git a/syft/pkg/deno.go b/syft/pkg/deno.go new file mode 100644 index 000000000..a3eedf95c --- /dev/null +++ b/syft/pkg/deno.go @@ -0,0 +1,19 @@ +package pkg + +// DenoLockEntry is a struct that rep a single entry found in the "packages" section of a Deno deno.lock file +type DenoLockEntry struct { + // Integrity is the crpto hash of the package content for verification + Integrity string `mapstructure:"integrity" json:"integrity"` + + // Dependencies is the list of package specifiers that this package depends on + Dependencies []string `mapstructure:"dependencies" json:"dependencies"` +} + +// DenoRemoteLockEntry is a struct that rep a single entry found in the "remote" section of a Deno deno.lock file +type DenoRemoteLockEntry struct { + // URL is the remote URL from which the module fetcef + URL string `mapstructure:"url" json:"url"` + + // Integrity is the crpto hash of the package content for verification + Integrity string `mapstructure:"integrity" json:"integrity"` +} From fe42bcec38f4460e7c7fee29d1020ccb084c5cc0 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 23 Jun 2026 15:23:46 -0400 Subject: [PATCH 024/168] fix(purl-backfill): respect arch qualifier (#4987) * fix(purl-backfill): respect arch qualifier Previously, when constructing rpm, alpm, and apk metadata struct from a PURL, Syft would ignore the arch qualifier. Start respecting that qualifier. Signed-off-by: Will Murphy * chore: fix static analysis Signed-off-by: Will Murphy * Clean up control flow in PURL backfill code Signed-off-by: Will Murphy --------- Signed-off-by: Will Murphy --- syft/format/internal/backfill.go | 111 +++++++- syft/format/internal/backfill_test.go | 241 ++++++++++++++++++ .../cyclonedxutil/helpers/component_test.go | 3 +- syft/format/purls/decoder_test.go | 36 +-- 4 files changed, 361 insertions(+), 30 deletions(-) diff --git a/syft/format/internal/backfill.go b/syft/format/internal/backfill.go index 8bb274a8b..8c7831d41 100644 --- a/syft/format/internal/backfill.go +++ b/syft/format/internal/backfill.go @@ -48,6 +48,7 @@ func backfillFromPurl(p *pkg.Package) { var cpes []cpe.CPE epoch := "" rpmmod := "" + arch := "" for _, qualifier := range purl.Qualifiers { switch qualifier.Key { @@ -65,6 +66,8 @@ func backfillFromPurl(p *pkg.Package) { epoch = qualifier.Value case pkg.PURLQualifierRpmModularity: rpmmod = qualifier.Value + case pkg.PURLQualifierArch: + arch = qualifier.Value } } @@ -84,9 +87,7 @@ func backfillFromPurl(p *pkg.Package) { setJavaMetadataFromPurl(p, purl) } - if p.Type == pkg.RpmPkg { - setRpmMetadataFromPurl(p, rpmmod) - } + setDistroMetadata(p, rpmmod, arch) for _, c := range cpes { if slices.Contains(p.CPEs, c) { @@ -96,6 +97,19 @@ func backfillFromPurl(p *pkg.Package) { } } +func setDistroMetadata(p *pkg.Package, rpmmod, arch string) { + switch p.Type { + case pkg.RpmPkg: + setRpmMetadataFromPurl(p, rpmmod, arch) + case pkg.DebPkg: + setDpkgMetadataFromPurl(p, arch) + case pkg.AlpmPkg: + setAlpmMetadataFromPurl(p, arch) + case pkg.ApkPkg: + setApkMetadataFromPurl(p, arch) + } +} + func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) { if p.Type != pkg.JavaPkg { return @@ -107,32 +121,103 @@ func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) { } } -func setRpmMetadataFromPurl(p *pkg.Package, rpmmod string) { +func setRpmMetadataFromPurl(p *pkg.Package, rpmmod, arch string) { if p.Type != pkg.RpmPkg { return } - if rpmmod == "" { + if rpmmod == "" && arch == "" { return } if p.Metadata == nil { - p.Metadata = pkg.RpmDBEntry{ - ModularityLabel: &rpmmod, - } - return + p.Metadata = pkg.RpmDBEntry{} } switch m := p.Metadata.(type) { case pkg.RpmDBEntry: - if m.ModularityLabel == nil { + if m.ModularityLabel == nil && rpmmod != "" { m.ModularityLabel = &rpmmod - p.Metadata = m } + if m.Arch == "" { + m.Arch = arch + } + p.Metadata = m case pkg.RpmArchive: - if m.ModularityLabel == nil { + if m.ModularityLabel == nil && rpmmod != "" { m.ModularityLabel = &rpmmod - p.Metadata = m } + if m.Arch == "" { + m.Arch = arch + } + p.Metadata = m + } +} + +func setDpkgMetadataFromPurl(p *pkg.Package, arch string) { + if p.Type != pkg.DebPkg { + return + } + if arch == "" { + return + } + + if p.Metadata == nil { + p.Metadata = pkg.DpkgDBEntry{} + } + + switch m := p.Metadata.(type) { + case pkg.DpkgDBEntry: + if m.Architecture == "" { + m.Architecture = arch + } + p.Metadata = m + case pkg.DpkgArchiveEntry: + if m.Architecture == "" { + m.Architecture = arch + } + p.Metadata = m + } +} + +func setAlpmMetadataFromPurl(p *pkg.Package, arch string) { + if p.Type != pkg.AlpmPkg { + return + } + if arch == "" { + return + } + + if p.Metadata == nil { + p.Metadata = pkg.AlpmDBEntry{Architecture: arch} + return + } + + if m, ok := p.Metadata.(pkg.AlpmDBEntry); ok { + if m.Architecture == "" { + m.Architecture = arch + } + p.Metadata = m + } +} + +func setApkMetadataFromPurl(p *pkg.Package, arch string) { + if p.Type != pkg.ApkPkg { + return + } + if arch == "" { + return + } + + if p.Metadata == nil { + p.Metadata = pkg.ApkDBEntry{Architecture: arch} + return + } + + if m, ok := p.Metadata.(pkg.ApkDBEntry); ok { + if m.Architecture == "" { + m.Architecture = arch + } + p.Metadata = m } } diff --git a/syft/format/internal/backfill_test.go b/syft/format/internal/backfill_test.go index 79918d6e9..beff255ae 100644 --- a/syft/format/internal/backfill_test.go +++ b/syft/format/internal/backfill_test.go @@ -39,6 +39,9 @@ func Test_Backfill(t *testing.T) { Type: pkg.RpmPkg, Name: "dbus-common", Version: "1.12.8-26.el8", + Metadata: pkg.RpmDBEntry{ + Arch: "noarch", + }, }, }, { @@ -51,6 +54,9 @@ func Test_Backfill(t *testing.T) { Type: pkg.RpmPkg, Name: "dbus-common", Version: "1:1.12.8-26.el8", + Metadata: pkg.RpmDBEntry{ + Arch: "noarch", + }, }, }, { @@ -65,6 +71,22 @@ func Test_Backfill(t *testing.T) { Version: "2.4.37-51", Metadata: pkg.RpmDBEntry{ ModularityLabel: strRef("httpd:2.4"), + Arch: "x86_64", + }, + }, + }, + { + name: "rpm with arch and no rpmmod", + in: pkg.Package{ + PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7", + }, + expected: pkg.Package{ + PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7", + Type: pkg.RpmPkg, + Name: "httpd", + Version: "2.4.37-51", + Metadata: pkg.RpmDBEntry{ + Arch: "x86_64", }, }, }, @@ -121,6 +143,225 @@ func Test_Backfill(t *testing.T) { Metadata: pkg.JavaArchive{}, }, }, + // deb cases + { + name: "deb nil metadata gets arch set", + in: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + }, + expected: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Type: pkg.DebPkg, + Name: "curl", + Version: "7.74.0-1.3+deb11u7", + Metadata: pkg.DpkgDBEntry{ + Architecture: "amd64", + }, + }, + }, + { + name: "deb arch:all preserved (nil metadata)", + in: pkg.Package{ + PURL: "pkg:deb/debian/tzdata@2021a-1+deb11u10?arch=all&distro=debian-11", + }, + expected: pkg.Package{ + PURL: "pkg:deb/debian/tzdata@2021a-1+deb11u10?arch=all&distro=debian-11", + Type: pkg.DebPkg, + Name: "tzdata", + Version: "2021a-1+deb11u10", + Metadata: pkg.DpkgDBEntry{ + Architecture: "all", + }, + }, + }, + { + name: "deb populated metadata empty arch gets filled", + in: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Metadata: pkg.DpkgDBEntry{ + Package: "curl", + }, + }, + expected: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Type: pkg.DebPkg, + Name: "curl", + Version: "7.74.0-1.3+deb11u7", + Metadata: pkg.DpkgDBEntry{ + Package: "curl", + Architecture: "amd64", + }, + }, + }, + { + name: "deb populated metadata with arch preserved", + in: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Metadata: pkg.DpkgDBEntry{ + Package: "curl", + Architecture: "arm64", + }, + }, + expected: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Type: pkg.DebPkg, + Name: "curl", + Version: "7.74.0-1.3+deb11u7", + Metadata: pkg.DpkgDBEntry{ + Package: "curl", + Architecture: "arm64", + }, + }, + }, + { + name: "deb archive entry empty arch gets filled", + in: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Metadata: pkg.DpkgArchiveEntry{ + Package: "curl", + }, + }, + expected: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Type: pkg.DebPkg, + Name: "curl", + Version: "7.74.0-1.3+deb11u7", + Metadata: pkg.DpkgArchiveEntry{ + Package: "curl", + Architecture: "amd64", + }, + }, + }, + { + name: "deb archive entry with arch preserved", + in: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Metadata: pkg.DpkgArchiveEntry{ + Package: "curl", + Architecture: "arm64", + }, + }, + expected: pkg.Package{ + PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11", + Type: pkg.DebPkg, + Name: "curl", + Version: "7.74.0-1.3+deb11u7", + Metadata: pkg.DpkgArchiveEntry{ + Package: "curl", + Architecture: "arm64", + }, + }, + }, + // alpm cases + { + name: "alpm nil metadata gets arch set", + in: pkg.Package{ + PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling", + }, + expected: pkg.Package{ + PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling", + Type: pkg.AlpmPkg, + Name: "curl", + Version: "7.88.1-1", + Metadata: pkg.AlpmDBEntry{ + Architecture: "x86_64", + }, + }, + }, + { + name: "alpm populated metadata empty arch gets filled", + in: pkg.Package{ + PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling", + Metadata: pkg.AlpmDBEntry{ + Package: "curl", + }, + }, + expected: pkg.Package{ + PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling", + Type: pkg.AlpmPkg, + Name: "curl", + Version: "7.88.1-1", + Metadata: pkg.AlpmDBEntry{ + Package: "curl", + Architecture: "x86_64", + }, + }, + }, + { + name: "alpm populated metadata with arch preserved", + in: pkg.Package{ + PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling", + Metadata: pkg.AlpmDBEntry{ + Package: "curl", + Architecture: "aarch64", + }, + }, + expected: pkg.Package{ + PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling", + Type: pkg.AlpmPkg, + Name: "curl", + Version: "7.88.1-1", + Metadata: pkg.AlpmDBEntry{ + Package: "curl", + Architecture: "aarch64", + }, + }, + }, + // apk cases + { + name: "apk nil metadata gets arch set", + in: pkg.Package{ + PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2", + }, + expected: pkg.Package{ + PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2", + Type: pkg.ApkPkg, + Name: "curl", + Version: "7.83.1-r2", + Metadata: pkg.ApkDBEntry{ + Architecture: "aarch64", + }, + }, + }, + { + name: "apk populated metadata empty arch gets filled", + in: pkg.Package{ + PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2", + Metadata: pkg.ApkDBEntry{ + Package: "curl", + }, + }, + expected: pkg.Package{ + PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2", + Type: pkg.ApkPkg, + Name: "curl", + Version: "7.83.1-r2", + Metadata: pkg.ApkDBEntry{ + Package: "curl", + Architecture: "aarch64", + }, + }, + }, + { + name: "apk populated metadata with arch preserved", + in: pkg.Package{ + PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2", + Metadata: pkg.ApkDBEntry{ + Package: "curl", + Architecture: "x86_64", + }, + }, + expected: pkg.Package{ + PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2", + Type: pkg.ApkPkg, + Name: "curl", + Version: "7.83.1-r2", + Metadata: pkg.ApkDBEntry{ + Package: "curl", + Architecture: "x86_64", + }, + }, + }, { name: "target-sw from CPE", in: pkg.Package{ diff --git a/syft/format/internal/cyclonedxutil/helpers/component_test.go b/syft/format/internal/cyclonedxutil/helpers/component_test.go index 341af7778..2b1f44421 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/component_test.go @@ -317,7 +317,7 @@ func Test_decodeComponent(t *testing.T) { }, }, }, - wantMetadata: pkg.RpmDBEntry{}, + wantMetadata: pkg.RpmDBEntry{Arch: "x86_64"}, wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", }, { @@ -341,6 +341,7 @@ func Test_decodeComponent(t *testing.T) { }, wantMetadata: pkg.RpmDBEntry{ Release: "some-release", + Arch: "x86_64", }, wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", }, diff --git a/syft/format/purls/decoder_test.go b/syft/format/purls/decoder_test.go index 8609bcb7a..cec0c3106 100644 --- a/syft/format/purls/decoder_test.go +++ b/syft/format/purls/decoder_test.go @@ -58,10 +58,11 @@ func TestDecoder_Decode(t *testing.T) { purl: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", expected: []pkg.Package{ { - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", + Name: "sysv-rc", + Version: "2.88dsf-59", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", + Metadata: pkg.DpkgDBEntry{Architecture: "all"}, }, }, }, @@ -91,10 +92,11 @@ func TestDecoder_Decode(t *testing.T) { purl: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", expected: []pkg.Package{ { - Name: "systemd-x", - Version: "239-82.el8_10.2", - Type: pkg.RpmPkg, - PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", + Name: "systemd-x", + Version: "239-82.el8_10.2", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", + Metadata: pkg.RpmDBEntry{Arch: "aarch64"}, }, }, }, @@ -102,10 +104,11 @@ func TestDecoder_Decode(t *testing.T) { purl: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", expected: []pkg.Package{ { - Name: "dbus-common", - Version: "1:1.12.8-26.el8", - Type: pkg.RpmPkg, - PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", + Name: "dbus-common", + Version: "1:1.12.8-26.el8", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", + Metadata: pkg.RpmDBEntry{Arch: "noarch"}, }, }, }, @@ -113,10 +116,11 @@ func TestDecoder_Decode(t *testing.T) { purl: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", expected: []pkg.Package{ { - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", + Name: "curl", + Version: "7.61.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", + Metadata: pkg.ApkDBEntry{Architecture: "aarch64"}, }, }, }, From abf6d78dfcab0580a3473d148cbd71a5c868ab20 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 24 Jun 2026 11:22:16 -0400 Subject: [PATCH 025/168] fixes the wrapped taskfile-tasks (#5013) Signed-off-by: Alex Goodman --- .make/go.mod | 6 +- .make/main.go | 157 +++++++++++++++----------------------------------- Taskfile.yaml | 6 +- 3 files changed, 53 insertions(+), 116 deletions(-) diff --git a/.make/go.mod b/.make/go.mod index 1af7544c8..317043a52 100644 --- a/.make/go.mod +++ b/.make/go.mod @@ -2,13 +2,11 @@ module github.com/anchore/syft/.make go 1.25.8 -require ( - github.com/anchore/go-make v0.7.0 - github.com/goccy/go-yaml v1.19.2 -) +require github.com/anchore/go-make v0.7.0 require ( github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/sys v0.46.0 // indirect ) diff --git a/.make/main.go b/.make/main.go index 274462506..90fcffea7 100644 --- a/.make/main.go +++ b/.make/main.go @@ -1,28 +1,19 @@ package main import ( - "fmt" - "os" "path/filepath" "runtime" - "github.com/goccy/go-yaml" - . "github.com/anchore/go-make" "github.com/anchore/go-make/file" - "github.com/anchore/go-make/git" "github.com/anchore/go-make/lang" "github.com/anchore/go-make/run" "github.com/anchore/go-make/tasks/golint" "github.com/anchore/go-make/tasks/goreleaser" + "github.com/anchore/go-make/tasks/gotask" "github.com/anchore/go-make/tasks/gotest" ) -// taskfileDescriptions maps Taskfile.yaml task names to their `desc:` field. -// Loaded at package init so wrap() can use Taskfile.yaml as the single source -// of truth for wrapped-task descriptions. -var taskfileDescriptions = mustReadTaskfileDescriptions() - func main() { Makefile( // shared anchore tasks @@ -89,110 +80,58 @@ func main() { Dependencies: Deps("static-analysis", "test", "install-test"), }, - // --- everything below is implemented in Taskfile.yaml and surfaced here - // via wrap(). Descriptions come from Taskfile.yaml (single source of truth). + // --- everything else is implemented in Taskfile.yaml. gotask.Tasks() + // discovers every (non-internal) Taskfile task — including the namespaced + // `generate:cpe-index:*` tasks from the included task.d file — and surfaces + // them as first-class go-make tasks (with their canonical names and `desc:`) + // that forward to `task `. Descriptions live in Taskfile.yaml as the + // single source of truth; no per-task wrapping needed here. + gotask.Tasks(), - // static analysis extras - wrap("check-json-schema-drift").RunOn("static-analysis"), - wrap("check-capability-drift"), - wrap("check-binary-fixture-size").RunOn("static-analysis"), + // gotask.Tasks() discovers canonical task names only, not Taskfile aliases, + // so re-expose `refresh-fixtures`'s `fixtures` alias for manual use. + Task{ + Name: "fixtures", + Description: "Clear and fetch all test fixture cache (alias of refresh-fixtures)", + Dependencies: Deps("refresh-fixtures"), + }, - // test extras - wrap("validate-cyclonedx-schema").RunOn("test"), - wrap("test-utils").RunOn("test"), - wrap("check-docker-cache").RunOn("test"), - wrap("snapshot-smoke-test"), - - // update commands - wrap("update-format-golden-files"), - - // fixture cache plumbing (heavy ORAS logic, lives in Taskfile). - // refresh-fixtures hooks into "unit" so `make unit` triggers the - // stale-cache detection + download just like `task unit` did on main - // (its `deps: [tmpdir, fixtures]` is what kept the fixture cache fresh). - wrap("fingerprints"), - wrap("refresh-fixtures").RunOn("unit"), - wrap("fixtures"), - wrap("build-fixtures"), - wrap("download-test-fixture-cache"), - wrap("upload-test-fixture-cache"), - wrap("show-test-image-cache"), - - // install-script tests (delegates to test/install/Makefile) - wrap("install-test"), - wrap("install-test-cache-save"), - wrap("install-test-cache-load"), - wrap("install-test-ci-mac"), - - // compare tests - wrap("generate-compare-file"), - wrap("compare-mac"), - wrap("compare-linux"), - wrap("compare-test-deb-package-install"), - wrap("compare-test-rpm-package-install"), - - // code/data generation (umbrella + per-target; each lives in Taskfile) - wrap("generate"), - wrap("generate-json-schema"), - wrap("generate-license-list"), - wrap("generate-cpe-dictionary-index"), - wrap("generate-capabilities"), - - // cleanup (each hooks into go-make's built-in `clean` label) - wrap("clean-snapshot").RunOn("clean"), - wrap("clean-docker-cache").RunOn("clean"), - wrap("clean-oras-cache").RunOn("clean"), - wrap("clean-cache").RunOn("clean"), - wrap("clean-test-observations").RunOn("clean"), + // gotask.Tasks() can't attach RunsOn labels, so wire the syft-specific + // Taskfile tasks into go-make's native phases here. These thin hooks have + // no body and no description (hidden from `make help`); they only pull the + // discovered tasks in when the labeled phase runs. + Task{ + Name: "static-analysis:syft", + RunsOn: lang.List("static-analysis"), + Dependencies: Deps("check-json-schema-drift", "check-binary-fixture-size"), + }, + Task{ + Name: "test:syft", + RunsOn: lang.List("test"), + Dependencies: Deps("validate-cyclonedx-schema", "test-utils", "check-docker-cache"), + }, + // refresh-fixtures hooks into "unit" so `make unit` triggers the stale-cache + // detection + download just like `task unit` did on main (its + // `deps: [tmpdir, fixtures]` is what kept the fixture cache fresh). + Task{ + Name: "unit:syft", + RunsOn: lang.List("unit"), + Dependencies: Deps("refresh-fixtures"), + }, + Task{ + Name: "clean:syft", + RunsOn: lang.List("clean"), + Dependencies: Deps( + "clean-snapshot", + "clean-docker-cache", + "clean-oras-cache", + "clean-cache", + "clean-test-observations", + ), + }, ) } -// wrap creates a go-make Task that delegates execution to `task `. The -// task's description is pulled from Taskfile.yaml's `desc:` field — descriptions -// for wrapped tasks must always live in Taskfile.yaml, never here. -func wrap(name string) Task { - desc, ok := taskfileDescriptions[name] - if !ok || desc == "" { - // loud-fail at startup so missing descs can't sneak through review. - panic(fmt.Sprintf("Taskfile.yaml task %q is missing a `desc:` field; please add one", name)) - } - return Task{ - Name: name, - Description: desc, - Run: func() { Run("task " + name) }, - } -} - -// mustReadTaskfileDescriptions parses Taskfile.yaml at the repo root and returns -// a map of task name -> desc. Runs at package init time so wrap() can use it. -func mustReadTaskfileDescriptions() map[string]string { - root := git.Root() - if root == "" { - return nil - } - path := filepath.Join(root, "Taskfile.yaml") - data, err := os.ReadFile(path) //nolint:gosec // G304: path resolved from git.Root() - if err != nil { - return nil - } - var tf struct { - Tasks map[string]struct { - Desc string `yaml:"desc"` - Aliases []string `yaml:"aliases"` - } `yaml:"tasks"` - } - lang.Throw(yaml.Unmarshal(data, &tf)) - out := make(map[string]string, len(tf.Tasks)) - for name, t := range tf.Tasks { - out[name] = t.Desc - // aliases inherit the canonical task's description so wrap() can find them. - for _, alias := range t.Aliases { - out[alias] = t.Desc - } - } - return out -} - // snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile: // /snapshot/-build__/syft, where arch maps amd64->amd64_v1 // and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming. diff --git a/Taskfile.yaml b/Taskfile.yaml index d3d59426b..d0ae4f20d 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -2,9 +2,9 @@ version: "3" # NOTE: most generic tasks (static-analysis, format, lint, unit, snapshot, release, # changelog, ci-release, etc.) are now provided natively by anchore/go-make and -# defined in .make/main.go. This file holds the syft-specific tasks that go-make -# wraps via `wrap("")` calls — keep descriptions (`desc:`) populated so they -# show up in `make help`. +# defined in .make/main.go. This file holds the syft-specific tasks, which go-make +# auto-discovers via gotask.Tasks() and surfaces as `make ` — keep +# descriptions (`desc:`) populated so they show up in `make help`. includes: generate:cpe-index: ./task.d/generate/cpe-index.yaml From 35d56bfb9904f2de841afdaa6062c05c67e22b19 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 25 Jun 2026 15:01:40 -0400 Subject: [PATCH 026/168] Update go-make to v0.8.0 (#5010) Signed-off-by: Alex Goodman --- .make/go.mod | 2 +- .make/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.make/go.mod b/.make/go.mod index 317043a52..787e6d0cc 100644 --- a/.make/go.mod +++ b/.make/go.mod @@ -2,7 +2,7 @@ module github.com/anchore/syft/.make go 1.25.8 -require github.com/anchore/go-make v0.7.0 +require github.com/anchore/go-make v0.8.0 require ( github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect diff --git a/.make/go.sum b/.make/go.sum index 2b15f37d8..72d002cfc 100644 --- a/.make/go.sum +++ b/.make/go.sum @@ -1,5 +1,5 @@ -github.com/anchore/go-make v0.7.0 h1:qosSwNWV/SsLFc1pI0DlrCZ2BUSDcGDcSKM6HdlnT6c= -github.com/anchore/go-make v0.7.0/go.mod h1:4M6TnArb5w693VyWsgr5dCWrk2BLNu/ed4JUcsrzS34= +github.com/anchore/go-make v0.8.0 h1:3XAD4WNHIbqY4kwFyCQJ7kKiYLzSzjB0UwzDVwRqlUY= +github.com/anchore/go-make v0.8.0/go.mod h1:4M6TnArb5w693VyWsgr5dCWrk2BLNu/ed4JUcsrzS34= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= From b15c5dbfe2bb21c9d73002c1056a829c8c411c75 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Thu, 25 Jun 2026 15:26:14 -0400 Subject: [PATCH 027/168] chore(deps): update anchore dependencies (#4960) * chore(deps): update anchore dependencies Signed-off-by: anchore-oss-update-bot * update snapshots Signed-off-by: Alex Goodman --------- Signed-off-by: anchore-oss-update-bot Signed-off-by: Alex Goodman Co-authored-by: anchore-oss-update-bot Co-authored-by: Alex Goodman --- .../handle_attestation_test.snap | 4 +- .../handle_fetch_image_test.snap | 4 +- .../handle_file_indexing_test.snap | 4 +- .../handle_pull_source_test.snap | 4 +- .../__snapshots__/handle_read_image_test.snap | 4 +- go.mod | 36 +++++----- go.sum | 72 +++++++++---------- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/cmd/syft/cli/ui/__snapshots__/handle_attestation_test.snap b/cmd/syft/cli/ui/__snapshots__/handle_attestation_test.snap index 7f99c078c..981402b26 100755 --- a/cmd/syft/cli/ui/__snapshots__/handle_attestation_test.snap +++ b/cmd/syft/cli/ui/__snapshots__/handle_attestation_test.snap @@ -1,6 +1,6 @@ [TestHandler_handleAttestationStarted/attesting_in_progress/task_line - 1] - ⠋ Creating a thing running a thing + ⠋ Creating a thing running a thing --- [TestHandler_handleAttestationStarted/attesting_in_progress/log - 1] @@ -11,7 +11,7 @@ --- [TestHandler_handleAttestationStarted/attesting_complete/task_line - 1] - ✔ Created a thing running a thing + ✔ Created a thing running a thing --- [TestHandler_handleAttestationStarted/attesting_complete/log - 1] diff --git a/cmd/syft/cli/ui/__snapshots__/handle_fetch_image_test.snap b/cmd/syft/cli/ui/__snapshots__/handle_fetch_image_test.snap index 409940d64..19e0b1e69 100755 --- a/cmd/syft/cli/ui/__snapshots__/handle_fetch_image_test.snap +++ b/cmd/syft/cli/ui/__snapshots__/handle_fetch_image_test.snap @@ -1,8 +1,8 @@ [TestHandler_handleFetchImage/fetch_image_in_progress - 1] - ⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image + ⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image --- [TestHandler_handleFetchImage/fetch_image_complete - 1] - ✔ Loaded image the-image + ✔ Loaded image the-image --- diff --git a/cmd/syft/cli/ui/__snapshots__/handle_file_indexing_test.snap b/cmd/syft/cli/ui/__snapshots__/handle_file_indexing_test.snap index a847c1df6..2f5968e8f 100755 --- a/cmd/syft/cli/ui/__snapshots__/handle_file_indexing_test.snap +++ b/cmd/syft/cli/ui/__snapshots__/handle_file_indexing_test.snap @@ -1,8 +1,8 @@ [TestHandler_handleFileIndexingStarted/cataloging_in_progress - 1] - ⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path + ⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path --- [TestHandler_handleFileIndexingStarted/cataloging_complete - 1] - ✔ Indexed file system /some/path + ✔ Indexed file system /some/path --- diff --git a/cmd/syft/cli/ui/__snapshots__/handle_pull_source_test.snap b/cmd/syft/cli/ui/__snapshots__/handle_pull_source_test.snap index c04614f74..905607ce9 100755 --- a/cmd/syft/cli/ui/__snapshots__/handle_pull_source_test.snap +++ b/cmd/syft/cli/ui/__snapshots__/handle_pull_source_test.snap @@ -1,8 +1,8 @@ [TestHandler_handlePullSourceStarted/snap_download_in_progress - 1] - ⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap + ⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap --- [TestHandler_handlePullSourceStarted/snap_download_complete - 1] - ✔ Snap downloaded successfully example-app_1.0_amd64.snap + ✔ Snap downloaded successfully example-app_1.0_amd64.snap --- diff --git a/cmd/syft/cli/ui/__snapshots__/handle_read_image_test.snap b/cmd/syft/cli/ui/__snapshots__/handle_read_image_test.snap index 0ec1bf256..ded3b615b 100755 --- a/cmd/syft/cli/ui/__snapshots__/handle_read_image_test.snap +++ b/cmd/syft/cli/ui/__snapshots__/handle_read_image_test.snap @@ -1,8 +1,8 @@ [TestHandler_handleReadImage/read_image_in_progress - 1] - ⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id + ⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id --- [TestHandler_handleReadImage/read_image_complete - 1] - ✔ Parsed image id + ✔ Parsed image id --- diff --git a/go.mod b/go.mod index 6e473cbf3..c7369689f 100644 --- a/go.mod +++ b/go.mod @@ -11,18 +11,18 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/acobaugh/osrelease v0.1.0 github.com/adrg/xdg v0.5.3 - github.com/anchore/bubbly v0.2.0 - github.com/anchore/clio v0.1.0 - github.com/anchore/fangs v0.1.0 - github.com/anchore/go-collections v0.1.0 - github.com/anchore/go-homedir v0.1.0 - github.com/anchore/go-logger v0.1.0 - github.com/anchore/go-macholibre v0.1.0 + github.com/anchore/bubbly v0.2.1 + github.com/anchore/clio v0.1.1 + github.com/anchore/fangs v0.1.1 + github.com/anchore/go-collections v0.1.1 + github.com/anchore/go-homedir v0.1.1 + github.com/anchore/go-logger v0.1.1 + github.com/anchore/go-macholibre v0.1.1 github.com/anchore/go-rpmdb v0.1.0 - github.com/anchore/go-sync v0.1.0 + github.com/anchore/go-sync v0.1.1 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.2.0 - github.com/anchore/stereoscope v0.2.1 + github.com/anchore/stereoscope v0.2.2 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef @@ -48,7 +48,7 @@ require ( github.com/goccy/go-yaml v1.19.2 github.com/gohugoio/hashstructure v0.6.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-containerregistry v0.21.6 + github.com/google/go-containerregistry v0.21.7 github.com/google/licensecheck v0.3.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.1 @@ -123,10 +123,10 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect github.com/Microsoft/hcsshim v0.15.0-rc.1 // indirect - github.com/ProtonMail/go-crypto v1.4.0 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/STARRY-S/zip v0.2.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect - github.com/anchore/go-lzo v0.1.0 // indirect + github.com/anchore/go-lzo v0.1.1 // indirect github.com/anchore/go-struct-converter v0.2.0-rc2 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect @@ -159,13 +159,13 @@ require ( github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/buger/jsonparser v1.1.2 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.10.0 // indirect - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect @@ -182,7 +182,7 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v29.4.3+incompatible // indirect + github.com/docker/cli v29.5.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -227,7 +227,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect @@ -283,7 +283,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/sylabs/sif/v2 v2.24.0 // indirect + github.com/sylabs/sif/v2 v2.24.1 // indirect github.com/sylabs/squashfs v1.0.6 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/gjson v1.19.0 // indirect diff --git a/go.sum b/go.sum index 7dc6eff9c..8dcd0c532 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065r github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= -github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -120,34 +120,34 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/bubbly v0.2.0 h1:VE6nLlWsYV+bTOqqCzS46NirrVmvHah3UQDtFcjzgHU= -github.com/anchore/bubbly v0.2.0/go.mod h1:zRg1mfRCCe4vq1t9Ibu24YurErmnp9LAVCbpnpsb1pQ= -github.com/anchore/clio v0.1.0 h1:lrlSgD38V0T6TLjOG5Kn3miEXCQyYr6ZlwTSnW1WIJg= -github.com/anchore/clio v0.1.0/go.mod h1:z4l6oHaU8x/yQ3BnES4Ekh4eLqFSYlj1jQpk7waPkJs= -github.com/anchore/fangs v0.1.0 h1:EidxQN2XDRWn8q2n9pCVZXrOEnvpV1CuPLiWMyWG/vE= -github.com/anchore/fangs v0.1.0/go.mod h1:5YocMj6a0MC+KiBXBP0/ocOfc54DvlsOcpkAxmPWgx0= -github.com/anchore/go-collections v0.1.0 h1:vh2LE/j6HLKC27pENkas+J+h4BK4nOte9b7pDjlhY9w= -github.com/anchore/go-collections v0.1.0/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= -github.com/anchore/go-homedir v0.1.0 h1:WP+lNLOz2og8hzCkY7IccyFNInVKsrjogzV19G5G8p0= -github.com/anchore/go-homedir v0.1.0/go.mod h1:9B0DGhbmMAJVGGEbrlb+PkORM5eDFCEOtZ3xQ22qaLA= -github.com/anchore/go-logger v0.1.0 h1:3pE9Sli4SE1I/mtV+2pZXzim+aWj83BRaneVXeMgD3A= -github.com/anchore/go-logger v0.1.0/go.mod h1:0l63X5VyHqfWQwsNaTmkgXErsfohze3NAFCXFdhSjdQ= -github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= -github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= -github.com/anchore/go-macholibre v0.1.0 h1:qHbdusBZNcZM/uuKf1Psa9xxAFSoyRTps8GW9gpJgsg= -github.com/anchore/go-macholibre v0.1.0/go.mod h1:eu0gbwaZ+ocVFJLePdmPPDKU8MboV1MKsUCr36Ckd5s= +github.com/anchore/bubbly v0.2.1 h1:b3ZNBefY8ks5DI4SQ7BXSbBIsN6eoaTD6s1su7NuCBo= +github.com/anchore/bubbly v0.2.1/go.mod h1:o6a9aH3/fCn+mb5g1T1tBbQZ4hGR0yO5/uD1d2Vtago= +github.com/anchore/clio v0.1.1 h1:9bkYnaFbpT0J3G0qSHyQqa/lpK5zV0C9Gtgh82H/jj0= +github.com/anchore/clio v0.1.1/go.mod h1:CFPIopJlakJbBpKtAL/EPpj4W9hgBO7PfFc8N2FAtlE= +github.com/anchore/fangs v0.1.1 h1:NBTHocmWM2/0gNhebpoE/wEB0Z0nxh2lESww2o/Uf5M= +github.com/anchore/fangs v0.1.1/go.mod h1:82oZuk4+RynOvAoxnXKmI1zO5xk/kbOw+5t4fAAPSsk= +github.com/anchore/go-collections v0.1.1 h1:kGlmSehs+ctTBZr+98F2+taHeolHy+XDx1Nczl8GZc8= +github.com/anchore/go-collections v0.1.1/go.mod h1:zgDbPSNDpfU47tVo7aW+2suG7+vXB1Sq1qtDhRJknQ0= +github.com/anchore/go-homedir v0.1.1 h1:KpaO5BPrVT7cVMcEZ9KfFfEi0iuo3DtagPGl0C744X0= +github.com/anchore/go-homedir v0.1.1/go.mod h1:9B0DGhbmMAJVGGEbrlb+PkORM5eDFCEOtZ3xQ22qaLA= +github.com/anchore/go-logger v0.1.1 h1:HPBNmQVBdYliwaFVUL7oJ3UvSlaoXAMj/ZagAIYaCpA= +github.com/anchore/go-logger v0.1.1/go.mod h1:ekWuh5BkZVwyXnyEd4fwL29N4pNLssgRsqh2+kFN/b8= +github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM= +github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= +github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M= +github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro= github.com/anchore/go-rpmdb v0.1.0 h1:Q8dc208/HYzCqhx0L1zurfm1UPil24hlo9NjkdFmLdE= github.com/anchore/go-rpmdb v0.1.0/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU= github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4= github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw= -github.com/anchore/go-sync v0.1.0 h1:1TEZM7jISrvtoBMOF79xP0caQKASAtgW1yKqc0EjyZg= -github.com/anchore/go-sync v0.1.0/go.mod h1:Iposeub0kHipoTei1icj4Tys0SJN+cCdxEnkS7bZUFs= +github.com/anchore/go-sync v0.1.1 h1:91SZ+YqUIIHmf2jPAYZPuHqM/ZqnK1pVJDWtET6+AJE= +github.com/anchore/go-sync v0.1.1/go.mod h1:3haGsk2BnaoVhsfqae8Kd0FKIAILE1Hw69P4Xo5iVBw= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= -github.com/anchore/stereoscope v0.2.1 h1:x9c4LCPGh53tKDAQ22RqEUftEnL7tphJavSRke/aICE= -github.com/anchore/stereoscope v0.2.1/go.mod h1:PYx3fD4lvBVsYoQ/fBdauhZ5hmkRrJgw1B73svKx7/U= +github.com/anchore/stereoscope v0.2.2 h1:SGTLGoF6GHmKEn9Bb6LYpzzgz9nhvMZgN/c4fTSQjYY= +github.com/anchore/stereoscope v0.2.2/go.mod h1:ylJXJKebLctP7u9ewguB1d0zUETJxvAA7r2DiuljDTM= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -248,8 +248,8 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -274,10 +274,10 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -339,8 +339,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= -github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= +github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -509,8 +509,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM= -github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo= +github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= +github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= @@ -691,8 +691,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -924,8 +924,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w= -github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88= +github.com/sylabs/sif/v2 v2.24.1 h1:OhTOfTwBaGXfbWYWXK9C6Pojma3A1bTaNJ6CEhyuKok= +github.com/sylabs/sif/v2 v2.24.1/go.mod h1:PRq9MoP0g+p0qE5ZRGrOhyVAUdHrPsAVmXHgoiW6VU0= github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA= From 1143c12a97529fbfc60cecdefb53b6e8d0c33d4b Mon Sep 17 00:00:00 2001 From: sputnik-mac Date: Mon, 29 Jun 2026 21:17:52 +0700 Subject: [PATCH 028/168] fix: add .bpl file extension support to PE/DLL cataloger (closes #4664) (#4688) Borland Package Library (.bpl) files are standard Windows PE/DLL files used in Delphi and C++Builder ecosystems. This adds the .bpl glob pattern to the PE file discovery so these files are cataloged alongside .dll and .exe files. --- syft/pkg/cataloger/dotnet/cataloger_test.go | 2 ++ syft/pkg/cataloger/dotnet/deps_binary_cataloger.go | 3 ++- .../pkg/cataloger/dotnet/testdata/glob-paths/src/something.bpl | 0 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/something.bpl diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go index 163e5718a..65076f9f1 100644 --- a/syft/pkg/cataloger/dotnet/cataloger_test.go +++ b/syft/pkg/cataloger/dotnet/cataloger_test.go @@ -32,6 +32,7 @@ func TestCataloger_Globs(t *testing.T) { fixture: "testdata/glob-paths", cataloger: NewDotnetPortableExecutableCataloger(), expected: []string{ + "src/something.bpl", "src/something.dll", "src/something.exe", }, @@ -41,6 +42,7 @@ func TestCataloger_Globs(t *testing.T) { fixture: "testdata/glob-paths", cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()), expected: []string{ + "src/something.bpl", "src/something.deps.json", "src/something.dll", "src/something.exe", diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index 105c98c47..fbf53e648 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -23,6 +23,7 @@ const ( depsJSONGlob = "**/*.deps.json" dllGlob = "**/*.dll" exeGlob = "**/*.exe" + bplGlob = "**/*.bpl" ) // depsBinaryCataloger will search for both deps.json evidence and PE file evidence to create packages. All packages @@ -472,7 +473,7 @@ func readDepsJSON(resolver file.Resolver, loc file.Location) (*depsJSON, error) // findPEFiles locates and parses all PE files (dll/exe). func findPEFiles(resolver file.Resolver) ([]logicalPE, error, error) { - peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob) + peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob, bplGlob) if err != nil { return nil, nil, fmt.Errorf("unable to find PE files: %w", err) } diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/something.bpl b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/something.bpl new file mode 100644 index 000000000..e69de29bb From 37fee88b5c66b05e13aa2966331fbdce44b79c4b Mon Sep 17 00:00:00 2001 From: Chris Greeno Date: Mon, 29 Jun 2026 15:22:38 +0100 Subject: [PATCH 029/168] feat(elixir): emit dependency relationships from mix.lock (#4985) adds dependency-of relationships between elixir locked packages, matching how other ecosystem catalogers (alpine, arch, debian, redhat, python) express the dependency graph via the shared dependency.Processor/Specifier mechanism. Signed-off-by: Chris Greeno --- internal/constants.go | 3 +- schema/json/schema-16.1.6.json | 4358 +++++++++++++++++ schema/json/schema-latest.json | 9 +- syft/pkg/cataloger/elixir/cataloger.go | 4 +- syft/pkg/cataloger/elixir/cataloger_test.go | 12 + syft/pkg/cataloger/elixir/dependency.go | 28 + syft/pkg/cataloger/elixir/dependency_test.go | 103 + syft/pkg/cataloger/elixir/parse_mix_lock.go | 33 +- .../cataloger/elixir/parse_mix_lock_test.go | 72 +- .../elixir/testdata/relationships/mix.lock | 5 + syft/pkg/elixir.go | 5 + 11 files changed, 4593 insertions(+), 39 deletions(-) create mode 100644 schema/json/schema-16.1.6.json create mode 100644 syft/pkg/cataloger/elixir/dependency.go create mode 100644 syft/pkg/cataloger/elixir/dependency_test.go create mode 100644 syft/pkg/cataloger/elixir/testdata/relationships/mix.lock diff --git a/internal/constants.go b/internal/constants.go index bff8cc31e..dccdfdfd7 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,7 +3,7 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.1.5" + JSONSchemaVersion = "16.1.6" // Changelog // 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). @@ -12,5 +12,6 @@ const ( // 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata // 16.1.4 - add BunLockEntry metadata type for bun.lock support // 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support + // 16.1.6 - add Dependencies to ElixirMixLockEntry metadata ) diff --git a/schema/json/schema-16.1.6.json b/schema/json/schema-16.1.6.json new file mode 100644 index 000000000..98796e99c --- /dev/null +++ b/schema/json/schema-16.1.6.json @@ -0,0 +1,4358 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.1.6/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object", + "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman." + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string", + "description": "Value is the CPE string identifier." + }, + "source": { + "type": "string", + "description": "Source is the source where this CPE was obtained or generated from." + } + }, + "type": "object", + "required": [ + "cpe" + ], + "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases." + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, + "Descriptor": { + "properties": { + "name": { + "type": "string", + "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")." + }, + "version": { + "type": "string", + "description": "Version is the version of the tool that generated this SBOM." + }, + "configuration": { + "description": "Configuration contains the tool configuration used during SBOM generation." + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation." + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array", + "description": "Artifacts is the list of packages discovered and placed into the catalog" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array", + "description": "note: must have omitempty" + }, + "source": { + "$ref": "#/$defs/Source", + "description": "Source represents the original object that was cataloged" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease", + "description": "Distro represents the Linux distribution that was detected from the source" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor", + "description": "Descriptor is a block containing self-describing information about syft" + }, + "schema": { + "$ref": "#/$defs/Schema", + "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "type": { + "type": "string", + "description": "Type is type of entry could be package or project for internal refs" + }, + "executables": { + "additionalProperties": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing." + }, + "appCpe": { + "type": "string", + "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages." + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for this file within the SBOM." + }, + "location": { + "$ref": "#/$defs/Coordinates", + "description": "Location is the file path and layer information where this file was found." + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry", + "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type." + }, + "contents": { + "type": "string", + "description": "Contents is the file contents for small files." + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains cryptographic hashes of the file contents." + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array", + "description": "Licenses contains license information discovered within this file." + }, + "executable": { + "$ref": "#/$defs/Executable", + "description": "Executable contains executable metadata if this file is a binary." + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Unknowns contains unknown fields for forward compatibility." + } + }, + "type": "object", + "required": [ + "id", + "location" + ], + "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages." + }, + "FileLicense": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or text as found in the file." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence", + "description": "Evidence contains supporting evidence for this license detection." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ], + "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression." + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer", + "description": "Confidence is the confidence score for this license detection (0-100)." + }, + "offset": { + "type": "integer", + "description": "Offset is the byte offset where the license text starts in the file." + }, + "extent": { + "type": "integer", + "description": "Extent is the length of the license text in bytes." + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ], + "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level." + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer", + "description": "Mode is the Unix file permission mode in octal format." + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")." + }, + "linkDestination": { + "type": "string", + "description": "LinkDestination is the target path for symbolic links." + }, + "userID": { + "type": "integer", + "description": "UserID is the file owner user ID." + }, + "groupID": { + "type": "integer", + "description": "GroupID is the file owner group ID." + }, + "mimeType": { + "type": "string", + "description": "MIMEType is the MIME type of the file contents." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ], + "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file." + }, + "GgufFileHeader": { + "properties": { + "ggufVersion": { + "type": "integer", + "description": "GGUFVersion is the GGUF format version (e.g., 3)" + }, + "fileSize": { + "type": "integer", + "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")" + }, + "quantization": { + "type": "string", + "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")" + }, + "parameters": { + "type": "integer", + "description": "Parameters is the number of model parameters (if present in header)" + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensors in the model" + }, + "header": { + "type": "object", + "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication." + }, + "metadataHash": { + "type": "string", + "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames." + }, + "parts": { + "items": { + "$ref": "#/$defs/GgufFileHeader" + }, + "type": "array", + "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)." + } + }, + "type": "object", + "required": [ + "ggufVersion", + "tensorCount" + ], + "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file." + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field." + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptBunLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of runtime dependencies and their version specifiers" + }, + "optionalDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "OptionalDependencies is a map of optional dependencies and their version specifiers" + }, + "peerDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "PeerDependencies is a map of peer dependencies and their version specifiers" + }, + "bin": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Bin is a map of binary names to the paths they are installed to" + }, + "os": { + "type": "string", + "description": "OS is the operating system constraint for the package (e.g. \"darwin\")" + }, + "cpu": { + "type": "string", + "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies", + "optionalDependencies", + "peerDependencies", + "bin", + "os", + "cpu" + ], + "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\"" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptPnpmLockEntry": { + "properties": { + "resolution": { + "$ref": "#/$defs/PnpmLockResolution", + "description": "Resolution is the resolution information for the package" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolution", + "dependencies" + ], + "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or expression as found." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URLs are URLs where license text or information can be found." + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array", + "description": "Locations are file locations where this license was discovered." + }, + "contents": { + "type": "string", + "description": "Contents is the full license text content." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ], + "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations." + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string", + "description": "PrettyName is a human-readable operating system name with version." + }, + "name": { + "type": "string", + "description": "Name is the operating system name without version information." + }, + "id": { + "type": "string", + "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")." + }, + "idLike": { + "$ref": "#/$defs/IDLikes", + "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from." + }, + "version": { + "type": "string", + "description": "Version is the operating system version including codename if available." + }, + "versionID": { + "type": "string", + "description": "VersionID is the operating system version number or identifier." + }, + "versionCodename": { + "type": "string", + "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")." + }, + "buildID": { + "type": "string", + "description": "BuildID is a build identifier for the operating system." + }, + "imageID": { + "type": "string", + "description": "ImageID is an identifier for container or cloud images." + }, + "imageVersion": { + "type": "string", + "description": "ImageVersion is the version for container or cloud images." + }, + "variant": { + "type": "string", + "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")." + }, + "variantID": { + "type": "string", + "description": "VariantID is the lower-case operating system variant identifier." + }, + "homeURL": { + "type": "string", + "description": "HomeURL is the homepage URL for the operating system." + }, + "supportURL": { + "type": "string", + "description": "SupportURL is the support or help URL for the operating system." + }, + "bugReportURL": { + "type": "string", + "description": "BugReportURL is the bug reporting URL for the operating system." + }, + "privacyPolicyURL": { + "type": "string", + "description": "PrivacyPolicyURL is the privacy policy URL for the operating system." + }, + "cpeName": { + "type": "string", + "description": "CPEName is the Common Platform Enumeration name for the operating system." + }, + "supportEnd": { + "type": "string", + "description": "SupportEnd is the end of support date or version identifier." + }, + "extendedSupport": { + "type": "boolean", + "description": "ExtendedSupport indicates whether extended security or support is available." + } + }, + "type": "object", + "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files." + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Arbitrary key-value pairs that can be used to annotate a location" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GgufFileHeader" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptBunLockEntry" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptPnpmLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PnpmLockResolution": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "integrity" + ], + "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmFileEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the file download URL" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest is the hash digest of the file hosted at the URL" + } + }, + "type": "object", + "required": [ + "url", + "digest" + ] + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests (for the base package without extras)" + }, + "marker": { + "type": "string", + "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment" + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications for the base package (without extras)" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPdmLockExtraVariant" + }, + "type": "array", + "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)" + } + }, + "type": "object", + "required": [ + "summary", + "files" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPdmLockExtraVariant": { + "properties": { + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependencies specific to this extras variant" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files specific to this variant (only populated if different from base)" + }, + "marker": { + "type": "string", + "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")" + } + }, + "type": "object", + "required": [ + "extras" + ], + "description": "PythonPdmLockExtraVariant represents a specific extras combination variant within a PDM lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string", + "description": "Parent is the ID of the parent artifact in this relationship." + }, + "child": { + "type": "string", + "description": "Child is the ID of the child artifact in this relationship." + }, + "type": { + "type": "string", + "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")." + }, + "metadata": { + "description": "Metadata contains additional relationship-specific metadata." + } + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ], + "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package." + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "Schema": { + "properties": { + "version": { + "type": "string", + "description": "Version is the JSON schema version for this document format." + }, + "url": { + "type": "string", + "description": "URL is the URL to the JSON schema definition document." + } + }, + "type": "object", + "required": [ + "version", + "url" + ], + "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format." + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for the analyzed source artifact." + }, + "name": { + "type": "string", + "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)." + }, + "version": { + "type": "string", + "description": "Version is the version of the analyzed artifact (e.g., image tag)." + }, + "supplier": { + "type": "string", + "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance." + }, + "type": { + "type": "string", + "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")." + }, + "metadata": { + "description": "Metadata contains additional source-specific metadata." + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 88cec5ca3..98796e99c 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.1.5/document", + "$id": "anchore.io/schema/syft/json/16.1.6/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -1267,6 +1267,13 @@ "pkgHashExt": { "type": "string", "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages." } }, "type": "object", diff --git a/syft/pkg/cataloger/elixir/cataloger.go b/syft/pkg/cataloger/elixir/cataloger.go index 884558e34..ab846e6d2 100644 --- a/syft/pkg/cataloger/elixir/cataloger.go +++ b/syft/pkg/cataloger/elixir/cataloger.go @@ -6,10 +6,12 @@ package elixir import ( "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" ) // NewMixLockCataloger returns a cataloger object for Elixir mix.lock files. func NewMixLockCataloger() pkg.Cataloger { return generic.NewCataloger("elixir-mix-lock-cataloger"). - WithParserByGlobs(parseMixLock, "**/mix.lock") + WithParserByGlobs(parseMixLock, "**/mix.lock"). + WithProcessors(dependency.Processor(mixLockDependencySpecifier)) } diff --git a/syft/pkg/cataloger/elixir/cataloger_test.go b/syft/pkg/cataloger/elixir/cataloger_test.go index 3f5fd6213..7c4b77edc 100644 --- a/syft/pkg/cataloger/elixir/cataloger_test.go +++ b/syft/pkg/cataloger/elixir/cataloger_test.go @@ -30,3 +30,15 @@ func TestCataloger_Globs(t *testing.T) { }) } } + +func TestCataloger_Relationships(t *testing.T) { + expectedRelationships := []string{ + "cowlib @ 2.11.0 (mix.lock) [dependency-of] cowboy @ 2.9.0 (mix.lock)", + "ranch @ 1.8.0 (mix.lock) [dependency-of] cowboy @ 2.9.0 (mix.lock)", + } + + pkgtest.NewCatalogTester(). + FromDirectory(t, "testdata/relationships"). + ExpectsRelationshipStrings(expectedRelationships). + TestCataloger(t, NewMixLockCataloger()) +} diff --git a/syft/pkg/cataloger/elixir/dependency.go b/syft/pkg/cataloger/elixir/dependency.go new file mode 100644 index 000000000..01de04e36 --- /dev/null +++ b/syft/pkg/cataloger/elixir/dependency.go @@ -0,0 +1,28 @@ +package elixir + +import ( + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" +) + +var _ dependency.Specifier = mixLockDependencySpecifier + +// mixLockDependencySpecifier declares that a mix.lock entry provides its own +// package name and requires the names listed in its dependency list, so the +// dependency processor can resolve dependency-of relationships between the +// locked packages. +func mixLockDependencySpecifier(p pkg.Package) dependency.Specification { + meta, ok := p.Metadata.(pkg.ElixirMixLockEntry) + if !ok { + log.Tracef("cataloger failed to extract mix.lock metadata for package %+v", p.Name) + return dependency.Specification{} + } + + return dependency.Specification{ + ProvidesRequires: dependency.ProvidesRequires{ + Provides: []string{p.Name}, + Requires: meta.Dependencies, + }, + } +} diff --git a/syft/pkg/cataloger/elixir/dependency_test.go b/syft/pkg/cataloger/elixir/dependency_test.go new file mode 100644 index 000000000..ebe051838 --- /dev/null +++ b/syft/pkg/cataloger/elixir/dependency_test.go @@ -0,0 +1,103 @@ +package elixir + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" +) + +func Test_extractMixLockDependencies(t *testing.T) { + tests := []struct { + name string + line string + want []string + }{ + { + name: "no dependencies", + line: ` "castore": {:hex, :castore, "0.1.17", "hash", [:mix], [], "hexpm", "ext"},`, + want: nil, + }, + { + name: "single dependency", + line: ` "esbuild": {:hex, :esbuild, "0.5.0", "hash", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ext"},`, + want: []string{"castore"}, + }, + { + name: "multiple dependencies", + line: ` "cowboy": {:hex, :cowboy, "2.9.0", "hash", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib]}, {:ranch, "1.8.0", [hex: :ranch]}], "hexpm", "ext"},`, + want: []string{"cowlib", "ranch"}, + }, + { + name: "git source is skipped like hex source", + line: ` "mydep": {:git, "https://github.com/example/mydep.git", "ref", [{:jason, "~> 1.0", [hex: :jason]}]},`, + want: []string{"jason"}, + }, + { + name: "not a package line", + line: `%{`, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, extractMixLockDependencies(tt.line)) + }) + } +} + +func Test_mixLockDependencySpecifier(t *testing.T) { + tests := []struct { + name string + p pkg.Package + want dependency.Specification + }{ + { + name: "provides its name and requires its dependencies", + p: pkg.Package{ + Name: "cowboy", + Metadata: pkg.ElixirMixLockEntry{ + Name: "cowboy", + Dependencies: []string{"cowlib", "ranch"}, + }, + }, + want: dependency.Specification{ + ProvidesRequires: dependency.ProvidesRequires{ + Provides: []string{"cowboy"}, + Requires: []string{"cowlib", "ranch"}, + }, + }, + }, + { + name: "no dependencies still provides its name", + p: pkg.Package{ + Name: "castore", + Metadata: pkg.ElixirMixLockEntry{ + Name: "castore", + }, + }, + want: dependency.Specification{ + ProvidesRequires: dependency.ProvidesRequires{ + Provides: []string{"castore"}, + }, + }, + }, + { + name: "wrong metadata type yields empty specification", + p: pkg.Package{ + Name: "mystery", + Metadata: pkg.RubyGemspec{}, + }, + want: dependency.Specification{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, mixLockDependencySpecifier(tt.p)) + }) + } +} diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock.go b/syft/pkg/cataloger/elixir/parse_mix_lock.go index cf811426d..b444cde2a 100644 --- a/syft/pkg/cataloger/elixir/parse_mix_lock.go +++ b/syft/pkg/cataloger/elixir/parse_mix_lock.go @@ -21,6 +21,14 @@ var _ generic.Parser = parseMixLock var mixLockDelimiter = regexp.MustCompile(`[%{}\n" ,:]+`) +// mixLockDependency matches each `{:name,` tuple opener on a mix.lock line. The +// first match is the entry's own source tuple (e.g. `{:hex, :name, ...}`); the +// remaining matches are the entry's dependency tuples within its dependency +// list (e.g. `[{:cowlib, ...}, {:ranch, ...}]`). Build-tool lists like +// `[:mix]` or `[:make, :rebar3]` and option keyword lists like +// `[hex: :cowlib, ...]` use bare atoms, not `{:atom,`, so they don't match. +var mixLockDependency = regexp.MustCompile(`\{\s*:(\w+)\s*,`) + // parseMixLock parses a mix.lock and returns the discovered Elixir packages. func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var errs error @@ -56,13 +64,30 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re packages = append(packages, newPackage( pkg.ElixirMixLockEntry{ - Name: name, - Version: version, - PkgHash: hash, - PkgHashExt: hashExt, + Name: name, + Version: version, + PkgHash: hash, + PkgHashExt: hashExt, + Dependencies: extractMixLockDependencies(line), }, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ), ) } } + +// extractMixLockDependencies returns the names of the packages depended on by +// the entry described on a single mix.lock line, by reading its dependency +// list. The entry's own source tuple (the first `{:atom,` on the line) is +// skipped; everything after it is a dependency. +func extractMixLockDependencies(line string) []string { + matches := mixLockDependency.FindAllStringSubmatch(line, -1) + if len(matches) <= 1 { + return nil + } + deps := make([]string, 0, len(matches)-1) + for _, m := range matches[1:] { + deps = append(deps, m[1]) + } + return deps +} diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go index 575620406..0e1e36e3c 100644 --- a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go +++ b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go @@ -48,10 +48,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/cowboy@2.9.0", Metadata: pkg.ElixirMixLockEntry{ - Name: "cowboy", - Version: "2.9.0", - PkgHash: "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", - PkgHashExt: "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde", + Name: "cowboy", + Version: "2.9.0", + PkgHash: "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", + PkgHashExt: "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde", + Dependencies: []string{"cowlib", "ranch"}, }, }, { @@ -62,10 +63,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/cowboy_telemetry@0.4.0", Metadata: pkg.ElixirMixLockEntry{ - Name: "cowboy_telemetry", - Version: "0.4.0", - PkgHash: "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", - PkgHashExt: "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de", + Name: "cowboy_telemetry", + Version: "0.4.0", + PkgHash: "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", + PkgHashExt: "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de", + Dependencies: []string{"cowboy", "telemetry"}, }, }, { @@ -90,10 +92,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/db_connection@2.4.2", Metadata: pkg.ElixirMixLockEntry{ - Name: "db_connection", - Version: "2.4.2", - PkgHash: "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", - PkgHashExt: "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668", + Name: "db_connection", + Version: "2.4.2", + PkgHash: "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", + PkgHashExt: "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668", + Dependencies: []string{"connection", "telemetry"}, }, }, { @@ -132,10 +135,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/ecto@3.8.1", Metadata: pkg.ElixirMixLockEntry{ - Name: "ecto", - Version: "3.8.1", - PkgHash: "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", - PkgHashExt: "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b", + Name: "ecto", + Version: "3.8.1", + PkgHash: "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", + PkgHashExt: "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b", + Dependencies: []string{"decimal", "jason", "telemetry"}, }, }, { @@ -146,10 +150,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/ecto_sql@3.8.1", Metadata: pkg.ElixirMixLockEntry{ - Name: "ecto_sql", - Version: "3.8.1", - PkgHash: "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", - PkgHashExt: "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525", + Name: "ecto_sql", + Version: "3.8.1", + PkgHash: "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", + PkgHashExt: "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525", + Dependencies: []string{"db_connection", "ecto", "myxql", "postgrex", "tds", "telemetry"}, }, }, { @@ -160,10 +165,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/esbuild@0.5.0", Metadata: pkg.ElixirMixLockEntry{ - Name: "esbuild", - Version: "0.5.0", - PkgHash: "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", - PkgHashExt: "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5", + Name: "esbuild", + Version: "0.5.0", + PkgHash: "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", + PkgHashExt: "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5", + Dependencies: []string{"castore"}, }, }, { @@ -174,10 +180,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/ex_doc@0.28.4", Metadata: pkg.ElixirMixLockEntry{ - Name: "ex_doc", - Version: "0.28.4", - PkgHash: "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", - PkgHashExt: "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed", + Name: "ex_doc", + Version: "0.28.4", + PkgHash: "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", + PkgHashExt: "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed", + Dependencies: []string{"earmark_parser", "makeup_elixir", "makeup_erlang"}, }, }, { @@ -216,10 +223,11 @@ func TestParseMixLock(t *testing.T) { Locations: locations, PURL: "pkg:hex/jason@1.3.0", Metadata: pkg.ElixirMixLockEntry{ - Name: "jason", - Version: "1.3.0", - PkgHash: "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", - PkgHashExt: "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac", + Name: "jason", + Version: "1.3.0", + PkgHash: "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", + PkgHashExt: "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac", + Dependencies: []string{"decimal"}, }, }, } diff --git a/syft/pkg/cataloger/elixir/testdata/relationships/mix.lock b/syft/pkg/cataloger/elixir/testdata/relationships/mix.lock new file mode 100644 index 000000000..d6554eabe --- /dev/null +++ b/syft/pkg/cataloger/elixir/testdata/relationships/mix.lock @@ -0,0 +1,5 @@ +%{ + "cowlib": {:hex, :cowlib, "2.11.0", "aaa", [:make, :rebar3], [], "hexpm", "bbb"}, + "ranch": {:hex, :ranch, "1.8.0", "ccc", [:make, :rebar3], [], "hexpm", "ddd"}, + "cowboy": {:hex, :cowboy, "2.9.0", "eee", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "fff"}, +} diff --git a/syft/pkg/elixir.go b/syft/pkg/elixir.go index b13d04d1f..7efa0926a 100644 --- a/syft/pkg/elixir.go +++ b/syft/pkg/elixir.go @@ -13,4 +13,9 @@ type ElixirMixLockEntry struct { // PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum) PkgHashExt string `mapstructure:"pkgHashExt" json:"pkgHashExt"` + + // Dependencies are the names of the packages this entry depends on, as + // declared in the entry's dependency list within mix.lock. Used to derive + // dependency-of relationships between locked packages. + Dependencies []string `mapstructure:"dependencies" json:"dependencies,omitempty"` } From 956858fc11ceb6f7bdeab01cc9393969dd875a38 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Mon, 29 Jun 2026 08:00:48 -0700 Subject: [PATCH 030/168] ruby/gemspec: resolve simple #{s.name}/#{s.version} interpolation (#4782) * ruby/gemspec: resolve simple #{s.name}/#{s.version} interpolation Reported in anchore/syft#4720: scanning projects that depend on gems like formatador leaks literal Ruby interpolation into the emitted SBOM, e.g. "externalReferences": [ { "url": "https://github.com/geemus/#{s.name}", "type": "website" } ] because formatador.gemspec uses s.homepage = "https://github.com/geemus/#{s.name}" and parseGemSpecEntries reads the file as plain text instead of evaluating it. The interpolation leaks through the captured homepage field and on into any externalReferences entry the cataloger produces. Dependency Track then rejects the whole BOM because '{' and '}' are not valid IRI-reference characters (RFC 3987). Add a post-parse pass that substitutes the common interpolation forms (#{s.name}, #{gem.name}, #{name}, and the matching #{*.version} variants) in captured string fields using values already parsed from the same gemspec. Anything still containing '#{' after best-effort substitution is an unresolvable Ruby expression, and for URL-like fields (currently just homepage) we drop the field entirely so the SBOM is always schema-valid; callers would rather miss a homepage URL than emit one that breaks downstream tools. Adds testdata/formatador.gemspec, a minimal real-world gemspec using the #{s.name} pattern, plus a new parser test asserting that the homepage field comes out fully resolved. Fixes #4720 Signed-off-by: Sai Asish Y Signed-off-by: Alex Goodman * improve test cases Signed-off-by: Alex Goodman --------- Signed-off-by: Sai Asish Y Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/ruby/parse_gemspec.go | 63 +++++++++++ syft/pkg/cataloger/ruby/parse_gemspec_test.go | 107 ++++++++++++++++++ .../ruby/testdata/formatador.gemspec | 18 +++ 3 files changed, 188 insertions(+) create mode 100644 syft/pkg/cataloger/ruby/testdata/formatador.gemspec diff --git a/syft/pkg/cataloger/ruby/parse_gemspec.go b/syft/pkg/cataloger/ruby/parse_gemspec.go index b14cee704..92b586987 100644 --- a/syft/pkg/cataloger/ruby/parse_gemspec.go +++ b/syft/pkg/cataloger/ruby/parse_gemspec.go @@ -29,6 +29,12 @@ type gemData struct { // match example: Al\u003Ex ---> 003E var unicodePattern = regexp.MustCompile(`\\u(?P[0-9A-F]{4})`) +// match the common Ruby string-interpolation forms gemspec authors use to build +// fields from the gem's own name/version: #{s.name}, #{gem.name}, #{spec.version}, +// bare #{name}, and the same with surrounding whitespace. The optional receiver +// (s./gem./spec./...) is discarded; only the trailing attribute is captured. +var rubyInterpolationPattern = regexp.MustCompile(`#\{\s*(?:\w+\.)?(name|version)\s*\}`) + var patterns = map[string]*regexp.Regexp{ // match example: name = "railties".freeze ---> railties "name": regexp.MustCompile(`.*\.name\s*=\s*["']{1}(?P.*)["']{1} *`), @@ -96,6 +102,8 @@ func parseGemSpecEntries(ctx context.Context, resolver file.Resolver, _ *generic } } + resolveRubyInterpolationsInFields(fields) + if fields["name"] != "" && fields["version"] != "" { var metadata gemData if err := mapstructure.Decode(fields, &metadata); err != nil { @@ -116,6 +124,61 @@ func parseGemSpecEntries(ctx context.Context, resolver file.Resolver, _ *generic return pkgs, nil, nil } +// resolveRubyInterpolationsInFields substitutes a handful of well-known +// Ruby string interpolation placeholders (#{s.name}, #{s.version}, and +// the equivalent #{gem.*} forms) in captured gemspec string fields using +// values already captured from the same file. Gemspec authors routinely +// write things like +// +// s.homepage = "https://github.com/foo/#{s.name}" +// +// which Ruby evaluates before loading the gem. Syft reads the gemspec as +// plain text, so without this pass the literal #{s.name} would leak into +// the SBOM and in particular break CycloneDX schema validation because +// '{' and '}' are not valid IRI characters (see anchore/syft#4720). +// +// We only resolve interpolations pointing at name/version (the values syft has +// already captured from the same file). Both the substitution and the drop +// below are driven by the same field list, so adding a URL-like field here +// keeps it protected from leaking unresolved interpolation. +func resolveRubyInterpolationsInFields(fields map[string]any) { + name, _ := fields["name"].(string) + version, _ := fields["version"].(string) + + // homepage is currently the only captured string field that flows into a + // schema-validated URL slot (CycloneDX externalReferences, SPDX homepage). + for _, key := range []string{"homepage"} { + v, ok := fields[key].(string) + if !ok || v == "" { + continue + } + + v = rubyInterpolationPattern.ReplaceAllStringFunc(v, func(match string) string { + switch rubyInterpolationPattern.FindStringSubmatch(match)[1] { + case "name": + if name != "" { + return name + } + case "version": + if version != "" { + return version + } + } + return match // leave unresolved; the field is dropped below + }) + + // anything still containing a '#{' is an unresolvable Ruby expression. + // Drop the field rather than emit a URL with '{'/'}', which fails + // CycloneDX IRI validation (see anchore/syft#4720); a missing homepage + // is preferable to a BOM downstream tools reject. + if strings.Contains(v, "#{") { + delete(fields, key) + continue + } + fields[key] = v + } +} + // renderUtf8 takes any string escaped string subsections from the ruby string and replaces those sections with the UTF8 runes. func renderUtf8(s string) string { fullReplacement := unicodePattern.ReplaceAllStringFunc(s, func(unicodeSection string) string { diff --git a/syft/pkg/cataloger/ruby/parse_gemspec_test.go b/syft/pkg/cataloger/ruby/parse_gemspec_test.go index 8ed52b834..51496aded 100644 --- a/syft/pkg/cataloger/ruby/parse_gemspec_test.go +++ b/syft/pkg/cataloger/ruby/parse_gemspec_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" @@ -35,3 +37,108 @@ func TestParseGemspec(t *testing.T) { pkgtest.TestFileParser(t, fixture, parseGemSpecEntries, []pkg.Package{expectedPkg}, nil) } + +func TestResolveRubyInterpolationsInFields(t *testing.T) { + tests := []struct { + name string + fields map[string]any + wantHomepage string // "" with wantDropped=true means the key should be absent + wantDropped bool + }{ + { + name: "resolves #{s.name}", + fields: map[string]any{"name": "formatador", "homepage": "https://github.com/geemus/#{s.name}"}, + wantHomepage: "https://github.com/geemus/formatador", + }, + { + name: "resolves #{s.version}", + fields: map[string]any{"version": "1.1.0", "homepage": "https://example.com/v/#{s.version}"}, + wantHomepage: "https://example.com/v/1.1.0", + }, + { + name: "resolves #{gem.name}", + fields: map[string]any{"name": "foo", "homepage": "https://x/#{gem.name}"}, + wantHomepage: "https://x/foo", + }, + { + name: "resolves #{spec.version}", + fields: map[string]any{"version": "2.0", "homepage": "https://x/#{spec.version}"}, + wantHomepage: "https://x/2.0", + }, + { + name: "resolves bare #{name}", + fields: map[string]any{"name": "foo", "homepage": "https://x/#{name}"}, + wantHomepage: "https://x/foo", + }, + { + name: "resolves with surrounding whitespace", + fields: map[string]any{"name": "foo", "homepage": "https://x/#{ s.name }"}, + wantHomepage: "https://x/foo", + }, + { + name: "resolves multiple interpolations in one field", + fields: map[string]any{"name": "foo", "version": "1.2", "homepage": "https://x/#{s.name}/#{s.version}"}, + wantHomepage: "https://x/foo/1.2", + }, + { + name: "drops field on unresolvable expression", + fields: map[string]any{"name": "foo", "homepage": "https://x/#{Time.now}"}, + wantDropped: true, + }, + { + name: "drops field when referenced value was not captured", + fields: map[string]any{"homepage": "https://x/#{s.name}"}, + wantDropped: true, + }, + { + name: "leaves plain field untouched", + fields: map[string]any{"name": "foo", "homepage": "https://bundler.io"}, + wantHomepage: "https://bundler.io", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolveRubyInterpolationsInFields(tt.fields) + got, present := tt.fields["homepage"].(string) + if tt.wantDropped { + assert.False(t, present, "expected homepage to be dropped, got %q", got) + return + } + assert.Equal(t, tt.wantHomepage, got) + }) + } +} + +// Regression test for https://github.com/anchore/syft/issues/4720: +// gemspecs routinely build URL fields from Ruby string interpolation +// (e.g. "https://github.com/geemus/#{s.name}"), and syft used to pass +// those interpolations through into the emitted SBOM, producing URLs +// containing `{` and `}` that fail CycloneDX IRI validation. +func TestParseGemspec_ResolvesRubyInterpolation(t *testing.T) { + fixture := "testdata/formatador.gemspec" + ctx := context.TODO() + locations := file.NewLocationSet(file.NewLocation(fixture)) + + expectedPkg := pkg.Package{ + Name: "formatador", + Version: "1.1.0", + PURL: "pkg:gem/formatador@1.1.0", + Locations: locations, + Type: pkg.GemPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), + ), + Language: pkg.Ruby, + Metadata: pkg.RubyGemspec{ + Name: "formatador", + Version: "1.1.0", + Files: []string{"lib/formatador.rb"}, + Authors: []string{"geemus (Wesley Beary)"}, + // #{s.name} should have been resolved to the captured name. + Homepage: "https://github.com/geemus/formatador", + }, + } + + pkgtest.TestFileParser(t, fixture, parseGemSpecEntries, []pkg.Package{expectedPkg}, nil) +} diff --git a/syft/pkg/cataloger/ruby/testdata/formatador.gemspec b/syft/pkg/cataloger/ruby/testdata/formatador.gemspec new file mode 100644 index 000000000..36aceba0e --- /dev/null +++ b/syft/pkg/cataloger/ruby/testdata/formatador.gemspec @@ -0,0 +1,18 @@ +# -*- encoding: utf-8 -*- +# stub: formatador 1.1.0 ruby lib + +Gem::Specification.new do |s| + s.name = "formatador".freeze + s.version = "1.1.0".freeze + + s.require_paths = ["lib".freeze] + s.authors = ["geemus (Wesley Beary)".freeze] + s.date = "2026-04-17".freeze + s.email = "geemus@gmail.com".freeze + s.files = ["lib/formatador.rb".freeze] + s.homepage = "https://github.com/geemus/#{s.name}".freeze + s.licenses = ["MIT".freeze] + s.required_ruby_version = Gem::Requirement.new(">= 0".freeze) + s.rubygems_version = "3.5.22".freeze + s.summary = "Ruby STDOUT text progress bar library".freeze +end From 1746e96ad3abc5c1383fb1d7d98451c3adacd251 Mon Sep 17 00:00:00 2001 From: Archy <6429715+archy-rock3t-cloud@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:16:50 +0200 Subject: [PATCH 031/168] fix: correct typos and update examples README (#4703) Signed-off-by: Artem Muterko --- README.md | 2 +- cmd/syft/internal/ui/ui.go | 2 +- examples/README.md | 4 +++- syft/pkg/cataloger/dotnet/parse_packages_lock.go | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b5e57d0ca..acf888e34 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@  Go Report Card   GitHub release   GitHub go.mod Go version  -  License: Apache-2.0  +  License: Apache-2.0   Join our Discourse   Follow on Mastodon 

diff --git a/cmd/syft/internal/ui/ui.go b/cmd/syft/internal/ui/ui.go index e0354b583..193768c51 100644 --- a/cmd/syft/internal/ui/ui.go +++ b/cmd/syft/internal/ui/ui.go @@ -100,7 +100,7 @@ func (m *UI) Teardown(force bool) error { }) // it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in - // a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal). + // a bad state (where Ctrl+C and other control characters no longer work for future processes in that terminal). m.program.Quit() _ = runWithTimeout(250*time.Millisecond, func() error { diff --git a/examples/README.md b/examples/README.md index 8b6f1d45c..4f518ac58 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,8 +5,10 @@ This directory contains examples of how to use the Syft API. - `create_simple_sbom`: Create a simple SBOM from scratch - `create_custom_sbom`: Create an SBOM using as much custom configuration as possible, including a custom cataloger implementation - `decode_sbom`: Take an existing SBOM file (of arbitrary format) and decode it into a Syft SBOM object +- `select_catalogers`: Example demonstrating how to select specific catalogers to run - `source_detection`: Shows how to detect what to catalog automatically from a user string (e.g. container image vs directory) -- `source_from_image`: Construct a source from a only a container image +- `source_from_image`: Construct a source from only a container image +- `source_from_registry`: Construct a source directly from a container registry You can run any of these examples from this directory with: diff --git a/syft/pkg/cataloger/dotnet/parse_packages_lock.go b/syft/pkg/cataloger/dotnet/parse_packages_lock.go index 97bb4df86..99535d9da 100644 --- a/syft/pkg/cataloger/dotnet/parse_packages_lock.go +++ b/syft/pkg/cataloger/dotnet/parse_packages_lock.go @@ -80,7 +80,7 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi for depNameVersion, dep := range allDependencies { parentPkg, ok := pkgMap[depNameVersion] if !ok { - log.Debugf("package \"%s\" not found in map of all pacakges", depNameVersion) + log.Debugf("package \"%s\" not found in map of all packages", depNameVersion) continue } From e388b5249d80495c0fee833eeeec47b9127b62f7 Mon Sep 17 00:00:00 2001 From: nadimz Date: Mon, 29 Jun 2026 17:54:44 +0200 Subject: [PATCH 032/168] Add support for MIT and Heimdal Kerberos 5 library detection (#4781) * Add support for MIT and Heimdal Kerberos 5 library detection Signed-off-by: Nadim Zubidat * support 2-component case Signed-off-by: Alex Goodman --------- Signed-off-by: Nadim Zubidat Signed-off-by: Alex Goodman Co-authored-by: Nadim Zubidat Co-authored-by: Alex Goodman --- syft/pkg/cataloger/binary/capabilities.yaml | 20 +++++++++++ .../binary/classifier_cataloger_test.go | 34 ++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 26 ++++++++++++++ .../7.8.0/linux-amd64/libkrb5.so.26.0.0 | Bin 0 -> 360 bytes .../krb5/1.17/linux-amd64/libkrb5.so.3.3 | Bin 0 -> 357 bytes .../krb5/1.18.4/linux-amd64/libkrb5.so.3.3 | Bin 0 -> 357 bytes .../pkg/cataloger/binary/testdata/config.yaml | 25 +++++++++++++ 7 files changed, 105 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/heimdal-krb5/7.8.0/linux-amd64/libkrb5.so.26.0.0 create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.17/linux-amd64/libkrb5.so.3.3 create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.18.4/linux-amd64/libkrb5.so.3.3 diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index c98e5c57e..a40db6e16 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -846,6 +846,26 @@ catalogers: cpes: - cpe:2.3:a:elastic:elastic_agent:*:*:*:*:*:*:*:* type: BinaryPkg + - method: glob + criteria: + - '**/libkrb5.so*' + packages: + - class: krb5-library + name: krb5 + purl: pkg:generic/krb5 + cpes: + - cpe:2.3:a:mit:kerberos_5:*:*:*:*:*:*:*:* + type: BinaryPkg + - method: glob + criteria: + - '**/libkrb5.so*' + packages: + - class: heimdal-krb5-library + name: heimdal-krb5 + purl: pkg:generic/heimdal-krb5 + cpes: + - cpe:2.3:a:heimdal_project:heimdal:*:*:*:*:*:*:*:* + type: BinaryPkg - method: glob criteria: - '**/java' diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index a57290a9b..ab5af2703 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -1620,6 +1620,40 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("haskell-cabal-binary"), }, }, + { + logicalFixture: "krb5/1.18.4/linux-amd64", + expected: pkg.Package{ + Name: "krb5", + Version: "1.18.4", + Type: "binary", + PURL: "pkg:generic/krb5@1.18.4", + Locations: locations("libkrb5.so.3.3"), + Metadata: metadata("krb5-library"), + }, + }, + { + // base releases brand with a 2-component version (e.g. "krb5-1.17-final 1.17") + logicalFixture: "krb5/1.17/linux-amd64", + expected: pkg.Package{ + Name: "krb5", + Version: "1.17", + Type: "binary", + PURL: "pkg:generic/krb5@1.17", + Locations: locations("libkrb5.so.3.3"), + Metadata: metadata("krb5-library"), + }, + }, + { + logicalFixture: "heimdal-krb5/7.8.0/linux-amd64", + expected: pkg.Package{ + Name: "heimdal-krb5", + Version: "7.8.0", + Type: "binary", + PURL: "pkg:generic/heimdal-krb5@7.8.0", + Locations: locations("libkrb5.so.26.0.0"), + Metadata: metadata("heimdal-krb5-library"), + }, + }, { logicalFixture: "nginx/1.25.1/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index c5229235b..52a9be871 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -1191,6 +1191,32 @@ func DefaultClassifiers() []binutils.Classifier { PURL: mustPURL("pkg:generic/elastic-agent@version"), CPEs: singleCPE("cpe:2.3:a:elastic:elastic_agent:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, + { + Class: "krb5-library", + FileGlob: "**/libkrb5.so*", + // [NUL]KRB5_BRAND: krb5-1.18.4-final 1.18.4 20210722 + // [NUL]KRB5_BRAND: krb5-1.17-final 1.17 20190108 (base releases brand as 2-component) + EvidenceMatcher: m.FileContentsVersionMatcher( + `\x00KRB5_BRAND:\s+krb5-[^\s]+\s+(?P[0-9]+(?:\.[0-9]+){1,2})(?:\s|$)`, + ), + Package: "krb5", + PURL: mustPURL("pkg:generic/krb5@version"), + CPEs: singleCPE("cpe:2.3:a:mit:kerberos_5:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "heimdal-krb5-library", + FileGlob: "**/libkrb5.so*", + // $Version: Heimdal 7.5.0 + // $Version: Heimdal 7.8.0 + // $Version: Heimdal 7.1 + // $Version: Heimdal 7.0.3 + EvidenceMatcher: m.FileContentsVersionMatcher( + `(?m)\$Version:\s+Heimdal\s+(?P[0-9]+(?:\.[0-9]+){1,2})(?:\s|$)`, + ), + Package: "heimdal-krb5", + PURL: mustPURL("pkg:generic/heimdal-krb5@version"), + CPEs: singleCPE("cpe:2.3:a:heimdal_project:heimdal:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, } return append(classifiers, defaultJavaClassifiers()...) diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/heimdal-krb5/7.8.0/linux-amd64/libkrb5.so.26.0.0 b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/heimdal-krb5/7.8.0/linux-amd64/libkrb5.so.26.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..8f7dd1d43403497f731592ecd18d227d4a49b788 GIT binary patch literal 360 zcmY+9O-sZu5Qcm1uNdea^iqDNl)4m!h-7rqZqXw+JyOi-AzClX_p+2M_gZbhH)_g5z2Vt9rHm6 z>VjgLArokM7z`zh32g!sK{75Ya-LJKJtdMzszZ=%hzb8>10A^$!#HR$Aa&LlYpBw+ zy1he5lpB=Ql3VLZ&B#%B&KWfvX%J%cZQFGn*vpFW`zEXfLP@Edp`zN$#MC@tJb05p z_Nph%-BowJdBb^$B~S2=!KS%6<$W&a3^>nazHMI0&(=IY+{3Y?=q@nh2fB5s^Yetu S+H4=Uurmu-#9WVGW&aJ~%WAU# literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.17/linux-amd64/libkrb5.so.3.3 b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.17/linux-amd64/libkrb5.so.3.3 new file mode 100644 index 0000000000000000000000000000000000000000..3c0856ed721c6c71a541b21fd30ce6746e44e911 GIT binary patch literal 357 zcmZvYO-jT-5QW_c(%>B$vJ)nBb*EF^S?SgXjlJh$%o)#-BROI0e?r!>T|;l>(NlI7Svjr)d!6m00)!y%6Mdy=}I zrRbBL;`Dtf?&1u$zuoslwzzX3hv&=$k&>D?0M>Q?%()yVkbxD zX1CQ3+uhaeb;IW$Y@h_(q_(5X=aowVR)8?8_v2s2=hxYy{CuA;yu3JF__*?J;xC+G BQ*!_S literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.18.4/linux-amd64/libkrb5.so.3.3 b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.18.4/linux-amd64/libkrb5.so.3.3 new file mode 100644 index 0000000000000000000000000000000000000000..71d35dd487b04a6e39fcc6e73164b067beb343d1 GIT binary patch literal 357 zcmZvYze)r_48|=4VelaauD)fG`M;f~op8nZ05Wrv@fOG3!(9+NAHmM|@)fM@d;rJw zusBSSUy_e}g!JKoT3)(%{}^>M4vlOi8*(0LYPl0AonVFfyJ^>QK*7eo+wW<**#*?5 zuTV~fRt$LERo;Y>aaJP)M|PrOR(M^W*Jw=GTFrmEY++ BRmuPW literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml index 41d402bc9..5f8fa88aa 100644 --- a/syft/pkg/cataloger/binary/testdata/config.yaml +++ b/syft/pkg/cataloger/binary/testdata/config.yaml @@ -1433,6 +1433,31 @@ from-images: platform: linux/amd64 paths: - /usr/lib/x86_64-linux-gnu/libQtCore.so.4.8.6 + + - name: krb5 + version: 1.18.4 + images: + - ref: apache/ozone-testkrb5:20230318-1@sha256:6a7eeac1ebd12e8968e34ab93fb8d21f2b92ae52bc0a85b662a2d41065f05d3a + platform: linux/amd64 + paths: + - /usr/lib/libkrb5.so.3.3 + + - name: krb5 + version: 1.17 + images: + - ref: mongo:4.4@sha256:4be76f674fc4b27859816811b8baa3c51830eb1dbf4ca81a51e26b79edd662ef + platform: linux/amd64 + paths: + - /usr/lib/x86_64-linux-gnu/libkrb5.so.3.3 + + - name: heimdal-krb5 + version: 7.8.0 + images: + - ref: nadimz/heimdal-krb5:7.8.0@sha256:23a5046493f3e97669353c18abcba6bd57fc9ddf53faa63b91b1a94f52f590e5 + platform: linux/amd64 + paths: + - /usr/lib/x86_64-linux-gnu/libkrb5.so.26.0.0 + - version: 1.36.4 images: - ref: envoyproxy/envoy:v1.36.4@sha256:ae31562b8cede20913a2d3d6a4f44c8479a50551e033cb8ef7bb8e38cec4b573 From a34f86fba1c226dc0f9fadc2aeb4571b234efeb2 Mon Sep 17 00:00:00 2001 From: sputnik-mac Date: Mon, 29 Jun 2026 22:55:51 +0700 Subject: [PATCH 033/168] fix(template): expose sprig date/time functions in Go templates (#4644) * fix(template): expose sprig date functions in Go templates Replace HermeticTxtFuncMap with TxtFuncMap to expose date/time functions (now, date, dateInZone, etc.) while still excluding security-sensitive env/expandenv functions. Users can now use date functions in templates, e.g.: {{ now | unixEpoch }} {{ now | date "2006-01-02" }} Fixes #2372 Signed-off-by: Sputnik-MAC Signed-off-by: Alex Goodman * invert to add functions to the hermetic set, not the other way around Signed-off-by: Alex Goodman --------- Signed-off-by: Sputnik-MAC Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/format/template/encoder.go | 9 +++++++++ syft/format/template/encoder_test.go | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/syft/format/template/encoder.go b/syft/format/template/encoder.go index e387601c3..08ed33a65 100644 --- a/syft/format/template/encoder.go +++ b/syft/format/template/encoder.go @@ -33,7 +33,16 @@ type encoder struct { func NewFormatEncoder(cfg EncoderConfig) (sbom.FormatEncoder, error) { // TODO: revisit this... should no template file be an error or simply render an empty result? or render the json output? // Note: do not check for the existence of the template file here, as the default encoder cannot provide one. + // start from the hermetic (repeatable) sprig map, which omits functions that reach into the + // environment or network (env, expandenv, getHostByName), then re-expose just the date/time + // helpers requested in issue #2372. Allowlisting here keeps the user-template trust boundary + // safe even if sprig adds new non-hermetic functions in the future. f := sprig.HermeticTxtFuncMap() + full := sprig.TxtFuncMap() + for _, name := range []string{"now", "date", "dateInZone", "dateModify", "date_in_zone", "date_modify", "htmlDate", "htmlDateInZone"} { + f[name] = full[name] + } + f["getLastIndex"] = func(collection any) int { if v := reflect.ValueOf(collection); v.Kind() == reflect.Slice { return v.Len() - 1 diff --git a/syft/format/template/encoder_test.go b/syft/format/template/encoder_test.go index 4418b20af..dc487730d 100644 --- a/syft/format/template/encoder_test.go +++ b/syft/format/template/encoder_test.go @@ -82,6 +82,27 @@ func TestFormatWithOptionAndHasField(t *testing.T) { ) } +func TestFuncMap_ExposesDateFunctions_ExcludesEnvAndNetwork(t *testing.T) { + enc, err := NewFormatEncoder(DefaultEncoderConfig()) + require.NoError(t, err) + + e, ok := enc.(encoder) + require.True(t, ok) + + // date/time functions (the reason for issue #2372) should be available + for _, name := range []string{"now", "date", "dateInZone", "dateModify", "unixEpoch"} { + _, exists := e.funcMap[name] + assert.Truef(t, exists, "expected date function %q to be available", name) + } + + // functions that reach into the environment or network must remain excluded. + // rand*/uuidv4 are also kept out to preserve hermetic (repeatable) output. + for _, name := range []string{"env", "expandenv", "getHostByName", "randAlphaNum", "uuidv4"} { + _, exists := e.funcMap[name] + assert.Falsef(t, exists, "expected non-hermetic function %q to be excluded", name) + } +} + func TestFormatWithoutOptions(t *testing.T) { f, err := NewFormatEncoder(DefaultEncoderConfig()) require.NoError(t, err) From e7f1a803e7587741a6133800329927280ee5098e Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Mon, 29 Jun 2026 13:52:55 -0400 Subject: [PATCH 034/168] =?UTF-8?q?fixed=20dotnet=20cataloger=20can't=20fi?= =?UTF-8?q?nd=20packages=20from=20deps.json=20in=20linux=20el=E2=80=A6=20(?= =?UTF-8?q?#4517)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed dotnet cataloger can't find packages from deps.json in linux elf, fixed #4514 Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * split bundle and PE concerns Signed-off-by: Alex Goodman * limit resource usage of readall call Signed-off-by: Alex Goodman * removed duplicat Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * make sure the first 4 bytes in elf arent lostt Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * revert readelfbundle func, check size of readdeps json Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * revert readelfbundle func, check size of readdeps json, fixed #4514 Co-authored-by: Alex Goodman Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * move dotnet net8 linux fixture to testdata convention Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman * address malformed elf size claims + add tests Signed-off-by: Alex Goodman * dont key off of cataloger name in testing Signed-off-by: Alex Goodman --------- Signed-off-by: Rez Moss Signed-off-by: Alex Goodman Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Alex Goodman Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- cmd/syft/internal/options/catalog.go | 4 +- .../cataloger/binary/pe_package_cataloger.go | 2 +- syft/pkg/cataloger/dotnet/cataloger_test.go | 182 ++++++- .../cataloger/dotnet/deps_binary_cataloger.go | 72 +++ syft/pkg/cataloger/dotnet/package.go | 22 +- syft/pkg/cataloger/dotnet/pe.go | 2 +- .../.gitignore | 2 + .../Dockerfile | 16 + .../src/Program.cs | 48 ++ .../src/dotnetapp.csproj | 15 + .../src/packages.lock.json | 459 ++++++++++++++++++ .../internal/{pe => dotnet/bundle}/bundle.go | 97 +--- .../internal/dotnet/bundle/bundle_elf.go | 83 ++++ .../internal/dotnet/bundle/bundle_elf_test.go | 63 +++ .../cataloger/internal/dotnet/pe/bundle.go | 103 ++++ .../internal/{ => dotnet}/pe/bundle_test.go | 0 .../cataloger/internal/{ => dotnet}/pe/pe.go | 0 .../internal/{ => dotnet}/pe/pe_test.go | 0 .../{ => dotnet}/pe/testdata/Makefile | 0 .../image-dotnet31-single-file/Dockerfile | 2 +- .../image-dotnet31-single-file/src/Program.cs | 0 .../src/hello.csproj | 0 .../image-dotnet5-single-file/Dockerfile | 2 +- .../image-dotnet5-single-file/src/Program.cs | 0 .../src/hello.csproj | 0 .../image-dotnet6-single-file/Dockerfile | 2 +- .../image-dotnet6-single-file/src/Program.cs | 0 .../src/hello.csproj | 0 .../dotnet/pe/testdata/image-net8-app | 1 + .../pe/testdata/image-net8-app-single-file | 1 + .../testdata/net8-app-single-file.deps.json | 0 .../internal/pe/testdata/image-net8-app | 1 - .../pe/testdata/image-net8-app-single-file | 1 - 33 files changed, 1070 insertions(+), 110 deletions(-) create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json rename syft/pkg/cataloger/internal/{pe => dotnet/bundle}/bundle.go (52%) create mode 100644 syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go create mode 100644 syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go create mode 100644 syft/pkg/cataloger/internal/dotnet/pe/bundle.go rename syft/pkg/cataloger/internal/{ => dotnet}/pe/bundle_test.go (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/pe.go (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/pe_test.go (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/Makefile (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/Dockerfile (95%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/src/Program.cs (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/src/hello.csproj (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/Dockerfile (95%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/src/Program.cs (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/src/hello.csproj (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/Dockerfile (95%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/src/Program.cs (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/src/hello.csproj (100%) create mode 120000 syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app create mode 120000 syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/net8-app-single-file.deps.json (100%) delete mode 120000 syft/pkg/cataloger/internal/pe/testdata/image-net8-app delete mode 120000 syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index bcb8ab66f..1b74df358 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -176,7 +176,9 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL). WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL). WithPropagateDLLClaimsToParents(cfg.Dotnet.PropagateDLLClaimsToParents). - WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected), + WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected). + WithExcludeProjectReferences(cfg.Dotnet.ExcludeProjectReferences), + Golang: golang.DefaultCatalogerConfig(). WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)). WithLocalModCacheDir(cfg.Golang.LocalModCacheDir). diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger.go b/syft/pkg/cataloger/binary/pe_package_cataloger.go index ba90d8835..16292be76 100644 --- a/syft/pkg/cataloger/binary/pe_package_cataloger.go +++ b/syft/pkg/cataloger/binary/pe_package_cataloger.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" - "github.com/anchore/syft/syft/pkg/cataloger/internal/pe" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/pe" ) // NewPEPackageCataloger returns a cataloger that interprets packages from DLL, EXE, and BPL files. diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go index 65076f9f1..04b2caf20 100644 --- a/syft/pkg/cataloger/dotnet/cataloger_test.go +++ b/syft/pkg/cataloger/dotnet/cataloger_test.go @@ -14,10 +14,11 @@ import ( func TestCataloger_Globs(t *testing.T) { tests := []struct { - name string - fixture string - cataloger pkg.Cataloger - expected []string + name string + fixture string + cataloger pkg.Cataloger + expected []string + ignoreUnfulfilled []string }{ { name: "obtain deps.json files", @@ -47,15 +48,21 @@ func TestCataloger_Globs(t *testing.T) { "src/something.dll", "src/something.exe", }, + // the binary cataloger probes executables by MIME type to find embedded bundles, + // but the glob fixtures aren't real binaries so those queries go unfulfilled + ignoreUnfulfilled: []string{"application/x-executable", "application/x-sharedlib"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pkgtest.NewCatalogTester(). + tester := pkgtest.NewCatalogTester(). FromDirectory(t, test.fixture). - ExpectsResolverContentQueries(test.expected). - TestCataloger(t, test.cataloger) + ExpectsResolverContentQueries(test.expected) + if len(test.ignoreUnfulfilled) > 0 { + tester = tester.IgnoreUnfulfilledPathResponses(test.ignoreUnfulfilled...) + } + tester.TestCataloger(t, test.cataloger) }) } } @@ -1105,6 +1112,167 @@ func TestCataloger(t *testing.T) { }, assertion: assertSingleFileDeployment, }, + { + name: "combined cataloger (single file linux)", + fixture: "image-net8-app-single-file-linux", + cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()), + expectedPkgs: []string{ + "Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.af @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.az @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.da @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.de @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.el @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.es @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.he @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.id @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.is @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.it @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp)", + "Newtonsoft.Json @ 13.0.3 (/app/dotnetapp)", + "dotnetapp @ 1.0.0 (/app/dotnetapp)", + "runtimepack.Microsoft.NETCore.App.Runtime.linux-musl-x64 @ 8.0.14 (/app/dotnetapp)", + }, + expectedRels: []string{ + "Humanizer @ 2.14.1 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.af @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.az @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.da @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.de @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.el @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.es @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.he @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.id @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.is @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.it @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.af @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.az @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.da @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.de @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.el @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.es @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.he @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.id @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.is @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.it @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Newtonsoft.Json @ 13.0.3 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)", + "runtimepack.Microsoft.NETCore.App.Runtime.linux-musl-x64 @ 8.0.14 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)", + }, + }, { name: "deps cataloger (self-contained)", fixture: "image-net8-app-self-contained", diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index fbf53e648..ea96b1710 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -1,6 +1,7 @@ package dotnet import ( + "bytes" "context" "fmt" "io" @@ -16,7 +17,9 @@ import ( "github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle" ) const ( @@ -26,6 +29,8 @@ const ( bplGlob = "**/*.bpl" ) +var elfMagic = []byte{0x7f, 'E', 'L', 'F'} + // depsBinaryCataloger will search for both deps.json evidence and PE file evidence to create packages. All packages // from both sources are raised up, but with one merge operation applied; If a deps.json package reference can be // correlated with a PE file, the PE file is attached to the package as supporting evidence. @@ -38,11 +43,16 @@ func (c depsBinaryCataloger) Name() string { } func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen + elfDepsJSONs, elfUnknowns := findELFBundledDepsJSON(resolver) + depJSONDocs, unknowns, err := findDepsJSON(resolver) if err != nil { return nil, nil, err } + depJSONDocs = append(depJSONDocs, elfDepsJSONs...) + unknowns = unknown.Join(unknowns, elfUnknowns) + peFiles, ldpeUnknownErr, err := findPEFiles(resolver) if err != nil { return nil, nil, err @@ -520,6 +530,68 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) { return ldpe, nil } +func findELFBundledDepsJSON(resolver file.Resolver) ([]logicalDepsJSON, error) { + locs, err := resolver.FilesByMIMEType("application/x-executable", "application/x-sharedlib") + if err != nil { + return nil, nil + } + + var depsJSONs []logicalDepsJSON + var unknownErr error + for _, loc := range locs { + doc, err := readELFBundledDepsJSON(resolver, loc) + if err != nil { + unknownErr = unknown.Append(unknownErr, loc, err) + continue + } + if doc != nil { + depsJSONs = append(depsJSONs, *doc) + } + } + + return depsJSONs, unknownErr +} + +func readELFBundledDepsJSON(resolver file.Resolver, loc file.Location) (*logicalDepsJSON, error) { + reader, err := resolver.FileContentsByLocation(loc) + if err != nil { + return nil, err + } + defer internal.CloseAndLogError(reader, loc.RealPath) + + header := make([]byte, 4) + if _, err := io.ReadFull(reader, header); err != nil { + return nil, nil + } + if !bytes.Equal(header, elfMagic) { + return nil, nil + } + + uReader, err := unionreader.GetUnionReader(reader) + if err != nil { + return nil, err + } + + depsJSON, err := bundle.ExtractDepsJSONFromELFBundle(uReader) + if err != nil { + return nil, err + } + + if depsJSON == "" { + return nil, nil + } + + doc, err := newDepsJSON(file.NewLocationReadCloser(loc, io.NopCloser(strings.NewReader(depsJSON)))) + if err != nil || doc == nil { + return nil, nil + } + + doc.Location = loc + lDoc := getLogicalDepsJSON(*doc, nil) + + return &lDoc, nil +} + func extractEmbeddedDeps(pe logicalPE) *logicalDepsJSON { doc, err := newDepsJSON(file.NewLocationReadCloser(pe.Location, io.NopCloser(strings.NewReader(pe.EmbeddedDepsJSON)))) if err != nil || doc == nil { diff --git a/syft/pkg/cataloger/dotnet/package.go b/syft/pkg/cataloger/dotnet/package.go index 9a11745fe..ac2361d60 100644 --- a/syft/pkg/cataloger/dotnet/package.go +++ b/syft/pkg/cataloger/dotnet/package.go @@ -20,6 +20,7 @@ var ( spaceRegex = regexp.MustCompile(`[\s\xa0]+`) numberRegex = regexp.MustCompile(`\d`) versionPunctuationRegex = regexp.MustCompile(`[.,]+`) + nonPrintableRegex = regexp.MustCompile(`[\x00-\x1f]`) ) type runtimeFamily string @@ -204,18 +205,29 @@ func cleanVersionResourceField(values ...string) string { return "" } +var ( + depsJSONPathRegex = regexp.MustCompile(`([^\\\/]+)\.deps\.json$`) + exePathRegex = regexp.MustCompile(`([^\\\/]+)\.exe$`) + singleFileRegex = regexp.MustCompile(`([^\\\/]+)$`) +) + func getDepsJSONFilePrefix(p string) string { - r := regexp.MustCompile(`([^\\\/]+)\.deps\.json$`) - match := r.FindStringSubmatch(p) + match := depsJSONPathRegex.FindStringSubmatch(p) if len(match) > 1 { return match[1] } - r = regexp.MustCompile(`([^\\\/]+)\.exe$`) - match = r.FindStringSubmatch(p) + match = exePathRegex.FindStringSubmatch(p) if len(match) > 1 { return match[1] } + + // Handle ELF + match = singleFileRegex.FindStringSubmatch(p) + if len(match) > 1 && !strings.Contains(match[1], ".") { + return match[1] + } + return "" } @@ -336,7 +348,7 @@ func spaceNormalize(value string) string { // Consolidate all whitespace. value = spaceRegex.ReplaceAllString(value, " ") // Remove non-printable characters. - value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "") + value = nonPrintableRegex.ReplaceAllString(value, "") // Consolidate again and trim. value = spaceRegex.ReplaceAllString(value, " ") value = strings.TrimSpace(value) diff --git a/syft/pkg/cataloger/dotnet/pe.go b/syft/pkg/cataloger/dotnet/pe.go index 77e433c06..a1257c56c 100644 --- a/syft/pkg/cataloger/dotnet/pe.go +++ b/syft/pkg/cataloger/dotnet/pe.go @@ -2,7 +2,7 @@ package dotnet import ( "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/pkg/cataloger/internal/pe" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/pe" ) // logicalPE represents a PE file within the context of a .NET project (considering the deps.json file). diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore new file mode 100644 index 000000000..b0b8376dc --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore @@ -0,0 +1,2 @@ +/app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile new file mode 100644 index 000000000..88095de5d --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile @@ -0,0 +1,16 @@ +# This is the same as the net8-app image, however, the entire .NET runtime is compiled into a single binary, residing with the application. +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build +ARG RUNTIME=linux-musl-x64 +WORKDIR /src + +COPY src/*.csproj . +COPY src/packages.lock.json . +RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode --force-evaluate + +COPY src/ . +RUN dotnet publish -r $RUNTIME -p:PublishSingleFile=true -p:RestorePackagesWithLockFile=true -p:ErrorOnDuplicatePublishOutputFiles=false --self-contained true -p:EnableCompressionInSingleFile=true -p:DebugSymbols=false -p:DebugType=None -o /app + + +FROM busybox +WORKDIR /app +COPY --from=build /app . \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs new file mode 100644 index 000000000..d1856dbc2 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs @@ -0,0 +1,48 @@ +using System; +using Humanizer; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +namespace IndirectDependencyExample +{ + class Program + { + static void Main(string[] args) + { + string runtimeInfo = "hello world!\n"; + Console.WriteLine(runtimeInfo); + + + Console.WriteLine($"\"this_is_a_test\" to title case: {"this_is_a_test".Humanize(LetterCasing.Title)}"); + + const string jsonString = @" + { + ""message"": ""Hello from JSON!"", + ""details"": { + ""timestamp"": ""2025-03-26T12:00:00Z"", + ""version"": ""1.0.0"", + ""metadata"": { + ""author"": ""Claude"", + ""environment"": ""Development"" + } + }, + ""items"": [ + { + ""id"": 1, + ""name"": ""Item One"" + }, + { + ""id"": 2, + ""name"": ""Item Two"" + } + ] + }"; + + JObject jsonObject = JObject.Parse(jsonString); + + string message = (string)jsonObject["message"]; + Console.WriteLine($"Message: {message}"); + } + } +} \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj new file mode 100644 index 000000000..de8b12f87 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + true + + + + + + + + diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json new file mode 100644 index 000000000..a0f55dc2c --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json @@ -0,0 +1,459 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Humanizer": { + "type": "Direct", + "requested": "[2.14.1, )", + "resolved": "2.14.1", + "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==", + "dependencies": { + "Humanizer.Core.af": "2.14.1", + "Humanizer.Core.ar": "2.14.1", + "Humanizer.Core.az": "2.14.1", + "Humanizer.Core.bg": "2.14.1", + "Humanizer.Core.bn-BD": "2.14.1", + "Humanizer.Core.cs": "2.14.1", + "Humanizer.Core.da": "2.14.1", + "Humanizer.Core.de": "2.14.1", + "Humanizer.Core.el": "2.14.1", + "Humanizer.Core.es": "2.14.1", + "Humanizer.Core.fa": "2.14.1", + "Humanizer.Core.fi-FI": "2.14.1", + "Humanizer.Core.fr": "2.14.1", + "Humanizer.Core.fr-BE": "2.14.1", + "Humanizer.Core.he": "2.14.1", + "Humanizer.Core.hr": "2.14.1", + "Humanizer.Core.hu": "2.14.1", + "Humanizer.Core.hy": "2.14.1", + "Humanizer.Core.id": "2.14.1", + "Humanizer.Core.is": "2.14.1", + "Humanizer.Core.it": "2.14.1", + "Humanizer.Core.ja": "2.14.1", + "Humanizer.Core.ko-KR": "2.14.1", + "Humanizer.Core.ku": "2.14.1", + "Humanizer.Core.lv": "2.14.1", + "Humanizer.Core.ms-MY": "2.14.1", + "Humanizer.Core.mt": "2.14.1", + "Humanizer.Core.nb": "2.14.1", + "Humanizer.Core.nb-NO": "2.14.1", + "Humanizer.Core.nl": "2.14.1", + "Humanizer.Core.pl": "2.14.1", + "Humanizer.Core.pt": "2.14.1", + "Humanizer.Core.ro": "2.14.1", + "Humanizer.Core.ru": "2.14.1", + "Humanizer.Core.sk": "2.14.1", + "Humanizer.Core.sl": "2.14.1", + "Humanizer.Core.sr": "2.14.1", + "Humanizer.Core.sr-Latn": "2.14.1", + "Humanizer.Core.sv": "2.14.1", + "Humanizer.Core.th-TH": "2.14.1", + "Humanizer.Core.tr": "2.14.1", + "Humanizer.Core.uk": "2.14.1", + "Humanizer.Core.uz-Cyrl-UZ": "2.14.1", + "Humanizer.Core.uz-Latn-UZ": "2.14.1", + "Humanizer.Core.vi": "2.14.1", + "Humanizer.Core.zh-CN": "2.14.1", + "Humanizer.Core.zh-Hans": "2.14.1", + "Humanizer.Core.zh-Hant": "2.14.1" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Humanizer.Core.af": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ar": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.az": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bg": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bn-BD": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.cs": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.da": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.de": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.el": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.es": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fa": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fi-FI": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr-BE": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.he": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hu": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hy": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.id": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.is": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.it": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ja": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ko-KR": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ku": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.lv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ms-MY": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.mt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb-NO": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ro": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ru": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr-Latn": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.th-TH": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.tr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Cyrl-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Latn-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.vi": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-CN": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hans": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hant": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + } + }, + "net8.0/win-x64": {} + } +} \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/pe/bundle.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go similarity index 52% rename from syft/pkg/cataloger/internal/pe/bundle.go rename to syft/pkg/cataloger/internal/dotnet/bundle/bundle.go index a360441d5..facb70e73 100644 --- a/syft/pkg/cataloger/internal/pe/bundle.go +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go @@ -1,8 +1,6 @@ -package pe +package bundle import ( - "bytes" - "debug/pe" "encoding/binary" "errors" "fmt" @@ -45,92 +43,8 @@ const ( dotNetFileTypeSymbols ) -// extractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle. -// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into -// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed -// in a placeholder location within the PE structure, pointing to the bundle header which contains -// file entry metadata. For V2+ bundles (.NET 5+), the header includes direct offsets to deps.json; -// for V1 bundles (.NET Core 3.x), we parse the manifest to locate it. -// -// ┌──────────────────────────────────┐ -// │ PE AppHost Binary │ Standard PE structure -// │ ... │ -// │ [8B offset][32B signature] │ Bundle marker (in placeholder within PE) -// │ ... │ -// ├──────────────────────────────────┤ -// │ Bundled Files │ Raw file contents (assemblies, deps.json, etc.) -// ├──────────────────────────────────┤ -// │ Bundle Header │ Version info, file count, deps.json offset (V2+) -// │ File Manifest │ Per-file: offset, size, type, path -// └──────────────────────────────────┘ -// -// Parsing strategy: -// 1. Search only the PE portion (using section headers) for the bundle signature -// 2. Read 8 bytes before signature to get header offset -// 3. Parse header to get deps.json location (V2+) or scan manifest entries (V1) -// -// See related documentation for more information: -// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md -// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/bundler.md -// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs -// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs -// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/header.h -// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h -// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h -func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) { - headerOffset, err := findBundleHeaderOffset(r, sections) - if err != nil { - return "", err - } - if headerOffset == 0 { - return "", nil // not a .NET single-file bundle - } - - return readDepsJSONFromBundleHeader(r, headerOffset) -} - -// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset. -// Returns 0 if no bundle marker is found (not a single-file bundle). -func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) { - peEndOffset := calculatePEEndOffset(sections) - - if _, err := r.Seek(0, io.SeekStart); err != nil { - return 0, err - } - - peData := make([]byte, peEndOffset) - n, err := io.ReadFull(r, peData) - if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { - return 0, err - } - peData = peData[:n] - - idx := bytes.Index(peData, dotNetBundleSignature) - if idx == -1 || idx < 8 { - return 0, nil - } - - // the header offset is stored in the 8 bytes immediately before the signature - headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx])) - return headerOffset, nil -} - -// calculatePEEndOffset determines where the PE structure ends based on section headers, -// adding padding for alignment. This bounds our search for the bundle marker. -func calculatePEEndOffset(sections []pe.SectionHeader32) int64 { - var peEndOffset int64 - for _, sec := range sections { - endOfSection := int64(sec.PointerToRawData) + int64(sec.SizeOfRawData) - if endOfSection > peEndOffset { - peEndOffset = endOfSection - } - } - // add buffer for alignment padding after sections - return peEndOffset + 4096 -} - -// readDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content. -func readDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) { +// ReadDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content. +func ReadDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) { if _, err := r.Seek(headerOffset, io.SeekStart); err != nil { return "", err } @@ -194,6 +108,9 @@ func read7BitEncodedInt(r io.Reader) (int, error) { // readDepsJSONAtOffset reads deps.json content at a specific offset using seeks (avoiding loading entire file) func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) { + if size <= 0 || size > 3*1024*1024 { // 3MB max deps.json size in dotnet bundle + return "", nil + } if _, err := r.Seek(offset, io.SeekStart); err != nil { return "", fmt.Errorf("failed to seek to deps.json at offset %d: %w", offset, err) } @@ -206,7 +123,7 @@ func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) { // findDepsJSONInManifest parses manifest entries to find deps.json (for V1 bundles or fallback) func findDepsJSONInManifest(r io.ReadSeeker, numFiles int32, majorVersion uint32) (string, error) { - for range numFiles { + for i := int32(0); i < numFiles; i++ { var offset, size int64 if err := binary.Read(r, binary.LittleEndian, &offset); err != nil { diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go new file mode 100644 index 000000000..8725a7d01 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go @@ -0,0 +1,83 @@ +package bundle + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "io" + + "github.com/anchore/syft/syft/internal/unionreader" +) + +// ExtractDepsJSONFromELFBundle extracts the deps.json content from a .net singlefile +// bundle contained within an ELF bin +func ExtractDepsJSONFromELFBundle(r unionreader.UnionReader) (string, error) { + headerOffset, err := findBundleHeaderOffsetInELF(r) + if err != nil || headerOffset == 0 { + return "", err + } + return ReadDepsJSONFromBundleHeader(r, headerOffset) +} + +func findBundleHeaderOffsetInELF(r unionreader.UnionReader) (int64, error) { + elfFile, err := elf.NewFile(r) + if err != nil { + return 0, nil + } + + elfEndOffset := calculateELFEndOffset(elfFile) + if elfEndOffset == 0 { + return 0, nil + } + + // clamp to the actual file size so a malformed ELF (with bogus segment/section + // offsets+sizes) can't drive an arbitrarily large allocation below. + fileSize, err := r.Seek(0, io.SeekEnd) + if err != nil { + return 0, err + } + if elfEndOffset > fileSize { + elfEndOffset = fileSize + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return 0, err + } + + searchData := make([]byte, elfEndOffset) + n, err := io.ReadFull(r, searchData) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return 0, err + } + searchData = searchData[:n] + + idx := bytes.Index(searchData, dotNetBundleSignature) + if idx == -1 || idx < 8 { + return 0, nil + } + + return int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx])), nil +} + +func calculateELFEndOffset(f *elf.File) int64 { + var endOffset int64 + + for _, prog := range f.Progs { + end := int64(prog.Off) + int64(prog.Filesz) + if end > endOffset { + endOffset = end + } + } + + for _, sec := range f.Sections { + if sec.Type == elf.SHT_NOBITS { + continue + } + end := int64(sec.Offset) + int64(sec.Size) + if end > endOffset { + endOffset = end + } + } + return endOffset + 4096 +} diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go new file mode 100644 index 000000000..7a49dbea0 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go @@ -0,0 +1,63 @@ +package bundle + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// readSeekCloser adapts a *bytes.Reader to the unionreader.UnionReader interface (adds Close). +type readSeekCloser struct { + *bytes.Reader +} + +func (readSeekCloser) Close() error { return nil } + +// buildELFWithHugeFilesz returns a minimal, parseable ELF64 whose single program header +// declares an absurd p_filesz. calculateELFEndOffset will compute a huge end offset; the +// clamp in findBundleHeaderOffsetInELF must keep the allocation bounded to the real file. +func buildELFWithHugeFilesz() []byte { + const ( + ehSize = 64 + phSize = 56 + phOff = ehSize + phCount = 1 + ) + buf := make([]byte, ehSize+phSize) + + // e_ident + copy(buf[0:4], []byte{0x7f, 'E', 'L', 'F'}) + buf[4] = 2 // ELFCLASS64 + buf[5] = 1 // ELFDATA2LSB + buf[6] = 1 // EV_CURRENT + + le := binary.LittleEndian + le.PutUint16(buf[16:], 2) // e_type = ET_EXEC + le.PutUint16(buf[18:], 0x3e) // e_machine = x86-64 + le.PutUint32(buf[20:], 1) // e_version + le.PutUint64(buf[32:], phOff) // e_phoff + le.PutUint16(buf[52:], ehSize) // e_ehsize + le.PutUint16(buf[54:], phSize) // e_phentsize + le.PutUint16(buf[56:], phCount) // e_phnum + // e_shoff/e_shnum left zero so no sections are parsed + + ph := buf[phOff:] + le.PutUint32(ph[0:], 1) // p_type = PT_LOAD + le.PutUint64(ph[16:], 0) // p_offset + le.PutUint64(ph[32:], 1<<60) // p_filesz (bogus, attacker-controlled) + le.PutUint64(ph[40:], 1<<60) // p_memsz + return buf +} + +func TestExtractDepsJSONFromELFBundle_MalformedFileszDoesNotOverAllocate(t *testing.T) { + data := buildELFWithHugeFilesz() + r := readSeekCloser{bytes.NewReader(data)} + + // must not OOM/panic on the bogus p_filesz, and find no bundle signature + content, err := ExtractDepsJSONFromELFBundle(r) + require.NoError(t, err) + assert.Empty(t, content) +} diff --git a/syft/pkg/cataloger/internal/dotnet/pe/bundle.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle.go new file mode 100644 index 000000000..791b476b2 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/pe/bundle.go @@ -0,0 +1,103 @@ +package pe + +import ( + "bytes" + "debug/pe" + "encoding/binary" + "errors" + "io" + + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle" +) + +// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles. +var dotNetBundleSignature = []byte{ + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae, +} + +// ExtractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle. +// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into +// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed +// in a placeholder location within the PE structure, pointing to the bundle header which contains +// file entry metadata. For V2+ bundles (.NET 5+), the header includes direct offsets to deps.json; +// for V1 bundles (.NET Core 3.x), we parse the manifest to locate it. +// +// ┌──────────────────────────────────┐ +// │ PE AppHost Binary │ Standard PE structure +// │ ... │ +// │ [8B offset][32B signature] │ Bundle marker (in placeholder within PE) +// │ ... │ +// ├──────────────────────────────────┤ +// │ Bundled Files │ Raw file contents (assemblies, deps.json, etc.) +// ├──────────────────────────────────┤ +// │ Bundle Header │ Version info, file count, deps.json offset (V2+) +// │ File Manifest │ Per-file: offset, size, type, path +// └──────────────────────────────────┘ +// +// Parsing strategy: +// 1. Search only the PE portion (using section headers) for the bundle signature +// 2. Read 8 bytes before signature to get header offset +// 3. Parse header to get deps.json location (V2+) or scan manifest entries (V1) +// +// See related documentation for more information: +// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md +// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/bundler.md +// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs +// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs +// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/header.h +// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h +// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h +func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) { + headerOffset, err := findBundleHeaderOffset(r, sections) + if err != nil { + return "", err + } + if headerOffset == 0 { + return "", nil // not a .NET single-file bundle + } + + return bundle.ReadDepsJSONFromBundleHeader(r, headerOffset) +} + +// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset. +// Returns 0 if no bundle marker is found (not a single-file bundle). +func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) { + peEndOffset := calculatePEEndOffset(sections) + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return 0, err + } + + peData := make([]byte, peEndOffset) + n, err := io.ReadFull(r, peData) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return 0, err + } + peData = peData[:n] + + idx := bytes.Index(peData, dotNetBundleSignature) + if idx == -1 || idx < 8 { + return 0, nil + } + + // the header offset is stored in the 8 bytes immediately before the signature + headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx])) + return headerOffset, nil +} + +// calculatePEEndOffset determines where the PE structure ends based on section headers, +// adding padding for alignment. This bounds our search for the bundle marker. +func calculatePEEndOffset(sections []pe.SectionHeader32) int64 { + var peEndOffset int64 + for _, sec := range sections { + endOfSection := int64(sec.PointerToRawData) + int64(sec.SizeOfRawData) + if endOfSection > peEndOffset { + peEndOffset = endOfSection + } + } + // add buffer for alignment padding after sections + return peEndOffset + 4096 +} diff --git a/syft/pkg/cataloger/internal/pe/bundle_test.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go similarity index 100% rename from syft/pkg/cataloger/internal/pe/bundle_test.go rename to syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go diff --git a/syft/pkg/cataloger/internal/pe/pe.go b/syft/pkg/cataloger/internal/dotnet/pe/pe.go similarity index 100% rename from syft/pkg/cataloger/internal/pe/pe.go rename to syft/pkg/cataloger/internal/dotnet/pe/pe.go diff --git a/syft/pkg/cataloger/internal/pe/pe_test.go b/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go similarity index 100% rename from syft/pkg/cataloger/internal/pe/pe_test.go rename to syft/pkg/cataloger/internal/dotnet/pe/pe_test.go diff --git a/syft/pkg/cataloger/internal/pe/testdata/Makefile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/Makefile similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/Makefile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/Makefile diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile similarity index 95% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile index dc7f30763..71fdc97f7 100644 --- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/core/sdk:3.1 AS build WORKDIR /src -COPY src/ . +COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ -p:SelfContained=true \ diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/Program.cs similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/Program.cs rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/Program.cs diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/hello.csproj similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/hello.csproj rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/hello.csproj diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile similarity index 95% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile index 71ad584de..d9ab0b574 100644 --- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:5.0 AS build WORKDIR /src -COPY src/ . +COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ -p:SelfContained=true \ diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/Program.cs similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/Program.cs rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/Program.cs diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/hello.csproj similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/hello.csproj rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/hello.csproj diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile similarity index 95% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile index 2089ef9e6..bf89e026f 100644 --- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src -COPY src/ . +COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ -p:SelfContained=true \ diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/Program.cs similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/Program.cs rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/Program.cs diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/hello.csproj similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/hello.csproj rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/hello.csproj diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app new file mode 120000 index 000000000..34cfd36fd --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app @@ -0,0 +1 @@ +../../../../dotnet/testdata/image-net8-app \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file new file mode 120000 index 000000000..964979916 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file @@ -0,0 +1 @@ +../../../../dotnet/testdata/image-net8-app-single-file \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/pe/testdata/net8-app-single-file.deps.json b/syft/pkg/cataloger/internal/dotnet/pe/testdata/net8-app-single-file.deps.json similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/net8-app-single-file.deps.json rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/net8-app-single-file.deps.json diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app b/syft/pkg/cataloger/internal/pe/testdata/image-net8-app deleted file mode 120000 index 497e7d17c..000000000 --- a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app +++ /dev/null @@ -1 +0,0 @@ -../../../dotnet/testdata/image-net8-app \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file b/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file deleted file mode 120000 index 884c00cff..000000000 --- a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file +++ /dev/null @@ -1 +0,0 @@ -../../../dotnet/testdata/image-net8-app-single-file \ No newline at end of file From deee79411a1ee0e3941dc06abcf6354955961128 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 29 Jun 2026 15:23:47 -0400 Subject: [PATCH 035/168] fix: composite action version parsing (#4616) Signed-off-by: Keith Zantow --- syft/pkg/cataloger/githubactions/package.go | 1 - .../githubactions/parse_composite_action.go | 70 ++++++++++++++++++- .../parse_composite_action_test.go | 22 ++++++ .../cataloger/githubactions/parse_workflow.go | 3 +- .../testdata/composite-action.yaml | 7 ++ 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/syft/pkg/cataloger/githubactions/package.go b/syft/pkg/cataloger/githubactions/package.go index cba6cea89..179957510 100644 --- a/syft/pkg/cataloger/githubactions/package.go +++ b/syft/pkg/cataloger/githubactions/package.go @@ -73,7 +73,6 @@ func parseStepUsageStatement(use, comment string) (string, string) { // if version looks like a commit hash and we have a comment, try to extract version from comment if version != "" && regexp.MustCompile(`^[0-9a-f]{7,}$`).MatchString(version) && comment != "" { - versionRegex := regexp.MustCompile(`v?\d+\.\d+\.\d+`) matches := versionRegex.FindStringSubmatch(comment) if len(matches) >= 1 { diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action.go b/syft/pkg/cataloger/githubactions/parse_composite_action.go index 96378815e..09949aa03 100644 --- a/syft/pkg/cataloger/githubactions/parse_composite_action.go +++ b/syft/pkg/cataloger/githubactions/parse_composite_action.go @@ -24,12 +24,19 @@ type compositeActionRunsDef struct { } func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - var ca compositeActionDef var errs error - if errs = yaml.NewDecoder(reader).Decode(&ca); errs != nil { + var node yaml.Node + if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil { + return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs) + } + + var ca compositeActionDef + if errs = node.Decode(&ca); errs != nil { return nil, nil, fmt.Errorf("unable to parse yaml composite action file: %w", errs) } + attachCompositeActionUsageComments(&node, ca.Runs.Steps) + // we use a collection to help with deduplication before raising to higher level processing pkgs := pkg.NewCollection() @@ -49,3 +56,62 @@ func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *g return pkgs.Sorted(), nil, errs } + +func attachCompositeActionUsageComments(node *yaml.Node, steps []stepDef) { + root := node + if root.Kind == yaml.DocumentNode && len(root.Content) > 0 { + root = root.Content[0] + } + if root.Kind != yaml.MappingNode { + return + } + + // find the "runs" key + for i := 0; i < len(root.Content); i += 2 { + key := root.Content[i] + value := root.Content[i+1] + if key.Value != "runs" || value.Kind != yaml.MappingNode { + continue + } + // find the "steps" key within runs + for j := 0; j < len(value.Content); j += 2 { + stepsKey := value.Content[j] + stepsValue := value.Content[j+1] + if stepsKey.Value != "steps" || stepsValue.Kind != yaml.SequenceNode { + continue + } + readSteps(stepsValue, steps) + } + } +} + +func readSteps(stepsValue *yaml.Node, steps []stepDef) { + // iterate over each step + for stepIdx, stepNode := range stepsValue.Content { + if stepNode.Kind != yaml.MappingNode { + continue + } + // find the "uses" key within the step + for k := 0; k < len(stepNode.Content); k += 2 { + usesKey := stepNode.Content[k] + usesValue := stepNode.Content[k+1] + if usesKey.Value != "uses" || usesValue.Kind != yaml.ScalarNode { + continue + } + comment := usesValue.LineComment + if comment == "" { + comment = usesValue.HeadComment + } + if comment == "" { + comment = usesValue.FootComment + } + if comment == "" { + continue + } + versionMatch := versionRegex.FindString(comment) + if versionMatch != "" && stepIdx < len(steps) { + steps[stepIdx].UsesComment = versionMatch + } + } + } +} diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go index 2ed607fe3..b089f939f 100644 --- a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go +++ b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go @@ -14,6 +14,28 @@ func Test_parseCompositeActionForActionUsage(t *testing.T) { fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) expected := []pkg.Package{ + { + Name: "actions/checkout", + Version: "11", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/checkout@11", + Metadata: pkg.GitHubActionsUseStatement{ + Value: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683", + Comment: "11", + }, + }, + { + Name: "actions/setup-go", + Version: "v5.1.0", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/setup-go@v5.1.0", + Metadata: pkg.GitHubActionsUseStatement{ + Value: "actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed", + Comment: "v5.1.0", + }, + }, { Name: "actions/setup-go", Version: "v4", diff --git a/syft/pkg/cataloger/githubactions/parse_workflow.go b/syft/pkg/cataloger/githubactions/parse_workflow.go index f784bce63..1957ac2f5 100644 --- a/syft/pkg/cataloger/githubactions/parse_workflow.go +++ b/syft/pkg/cataloger/githubactions/parse_workflow.go @@ -19,6 +19,8 @@ var ( _ generic.Parser = parseWorkflowForWorkflowUsage ) +var versionRegex = regexp.MustCompile(`v?\d+(\.\d+)*`) + type workflowDef struct { Jobs map[string]workflowJobDef `yaml:"jobs"` } @@ -185,7 +187,6 @@ func processUsesNode(node *yaml.Node, wf *workflowDef, currentJob *string, curre } if comment != "" { - versionRegex := regexp.MustCompile(`v?\d+(\.\d+)*`) versionMatch := versionRegex.FindString(comment) if versionMatch != "" { diff --git a/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml b/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml index b294c90ae..8e8038b12 100644 --- a/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml +++ b/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml @@ -35,6 +35,13 @@ runs: path: ${{ github.workspace }}/.tmp key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v 11 + + - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed #v5.1.0 + with: + go-version: ${{ inputs.go-version }} + # note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in # some installations of project tools. - name: Restore go module cache From 148fe572bcd63ba1d9feffe4c15e3003e5f2acfd Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Tue, 30 Jun 2026 10:32:57 -0400 Subject: [PATCH 036/168] added macOS .app cataloger (#4490) * added macOS .app cataloger, fixed #4010 Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * added macOS .app cataloger, fixed #4010 Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * added macOS .app cataloger, fixed #4010 Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * address review comments Signed-off-by: Alex Goodman * bump schema to 16.1.7 Signed-off-by: Alex Goodman * address static analysis failures Signed-off-by: Alex Goodman * migrate to testdata Signed-off-by: Alex Goodman * expand fields and improve test coverage Signed-off-by: Alex Goodman --------- Signed-off-by: Rez Moss Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- .../test/integration/catalog_packages_test.go | 2 + go.mod | 1 + go.sum | 4 + internal/constants.go | 3 +- internal/packagemetadata/generated.go | 1 + internal/packagemetadata/names.go | 1 + internal/task/package_tasks.go | 2 + schema/json/schema-16.1.7.json | 4422 +++++++++++++++++ schema/json/schema-latest.json | 66 +- .../helpers/originator_supplier_test.go | 1 + .../internal/spdxutil/helpers/source_info.go | 2 + .../spdxutil/helpers/source_info_test.go | 8 + syft/pkg/apple.go | 44 + syft/pkg/cataloger/apple/capabilities.yaml | 42 + syft/pkg/cataloger/apple/cataloger.go | 12 + syft/pkg/cataloger/apple/cataloger_test.go | 180 + syft/pkg/cataloger/apple/package.go | 20 + syft/pkg/cataloger/apple/parse_info_plist.go | 91 + .../Slack.app/Contents/Info.plist | 26 + .../Ghostty.app/Contents/Info.plist | 30 + .../testdata/parse-cases/malformed.plist | 1 + .../parse-cases/name-from-executable.plist | 10 + .../parse-cases/name-from-identifier.plist | 10 + .../apple/testdata/parse-cases/no-name.plist | 8 + .../testdata/parse-cases/no-version.plist | 8 + .../version-from-bundle-version.plist | 10 + syft/pkg/type.go | 2 + syft/pkg/type_test.go | 5 +- 28 files changed, 5008 insertions(+), 4 deletions(-) create mode 100644 schema/json/schema-16.1.7.json create mode 100644 syft/pkg/apple.go create mode 100644 syft/pkg/cataloger/apple/capabilities.yaml create mode 100644 syft/pkg/cataloger/apple/cataloger.go create mode 100644 syft/pkg/cataloger/apple/cataloger_test.go create mode 100644 syft/pkg/cataloger/apple/package.go create mode 100644 syft/pkg/cataloger/apple/parse_info_plist.go create mode 100644 syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist create mode 100644 syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go index 53b0f4757..9abbb338d 100644 --- a/cmd/syft/internal/test/integration/catalog_packages_test.go +++ b/cmd/syft/internal/test/integration/catalog_packages_test.go @@ -89,6 +89,7 @@ func TestPkgCoverageImage(t *testing.T) { definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead definedPkgs.Remove(string(pkg.CondaPkg)) definedPkgs.Remove(string(pkg.ModelPkg)) + definedPkgs.Remove(string(pkg.AppleAppBundlePkg)) var cases []testCase cases = append(cases, commonTestCases...) @@ -164,6 +165,7 @@ func TestPkgCoverageDirectory(t *testing.T) { definedPkgs.Remove(string(pkg.CondaPkg)) definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages definedPkgs.Remove(string(pkg.ModelPkg)) + definedPkgs.Remove(string(pkg.AppleAppBundlePkg)) // for directory scans we should not expect to see any of the following package types definedPkgs.Remove(string(pkg.KbPkg)) diff --git a/go.mod b/go.mod index c7369689f..74adff89d 100644 --- a/go.mod +++ b/go.mod @@ -104,6 +104,7 @@ require ( require ( github.com/pb33f/ordered-map/v2 v2.3.1 github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd + howett.net/plist v1.0.1 ) require ( diff --git a/go.sum b/go.sum index 8dcd0c532..5b38989f4 100644 --- a/go.sum +++ b/go.sum @@ -626,6 +626,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -1489,6 +1490,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1509,6 +1511,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= diff --git a/internal/constants.go b/internal/constants.go index dccdfdfd7..3f27e106c 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,7 +3,7 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.1.6" + JSONSchemaVersion = "16.1.7" // Changelog // 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). @@ -13,5 +13,6 @@ const ( // 16.1.4 - add BunLockEntry metadata type for bun.lock support // 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support // 16.1.6 - add Dependencies to ElixirMixLockEntry metadata + // 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger ) diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go index eaa9643d3..1d39a8288 100644 --- a/internal/packagemetadata/generated.go +++ b/internal/packagemetadata/generated.go @@ -9,6 +9,7 @@ func AllTypes() []any { return []any{ pkg.AlpmDBEntry{}, pkg.ApkDBEntry{}, + pkg.AppleAppBundleEntry{}, pkg.BinarySignature{}, pkg.BitnamiSBOMEntry{}, pkg.BunLockEntry{}, diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go index 3e8d9f174..408667974 100644 --- a/internal/packagemetadata/names.go +++ b/internal/packagemetadata/names.go @@ -124,6 +124,7 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.SnapEntry{}, "snap-entry"), jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"), jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"), + jsonNames(pkg.AppleAppBundleEntry{}, "apple-app-bundle-entry"), jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"), jsonNames(pkg.TerraformLockProviderEntry{}, "terraform-lock-provider-entry"), jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"), diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index 016042424..71e9e3af6 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -5,6 +5,7 @@ import ( "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/ai" "github.com/anchore/syft/syft/pkg/cataloger/alpine" + "github.com/anchore/syft/syft/pkg/cataloger/apple" "github.com/anchore/syft/syft/pkg/cataloger/arch" "github.com/anchore/syft/syft/pkg/cataloger/binary" bitnamiSbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/bitnami" @@ -177,6 +178,7 @@ func DefaultPackageTaskFactories() Factories { newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"), newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"), newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"), + newSimplePackageTaskFactory(apple.NewAppBundleCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "apple"), newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"), newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"), newSimplePackageTaskFactory(ai.NewGGUFCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "gguf", "ml"), diff --git a/schema/json/schema-16.1.7.json b/schema/json/schema-16.1.7.json new file mode 100644 index 000000000..71e1dd2ff --- /dev/null +++ b/schema/json/schema-16.1.7.json @@ -0,0 +1,4422 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.1.7/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object", + "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman." + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "AppleAppBundleEntry": { + "properties": { + "bundleIdentifier": { + "type": "string", + "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")" + }, + "name": { + "type": "string", + "description": "Name is the short name of the bundle" + }, + "displayName": { + "type": "string", + "description": "DisplayName is the user-facing name of the bundle" + }, + "executable": { + "type": "string", + "description": "Executable is the name of the executable within the bundle" + }, + "shortVersion": { + "type": "string", + "description": "ShortVersion is the release (marketing) version of the bundle" + }, + "version": { + "type": "string", + "description": "Version is the build version of the bundle, which often differs from the short version" + }, + "packageType": { + "type": "string", + "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)" + }, + "supportedPlatforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")" + }, + "minimumSystemVersion": { + "type": "string", + "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle" + }, + "minimumOSVersion": { + "type": "string", + "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)" + }, + "copyright": { + "type": "string", + "description": "Copyright is the human-readable copyright notice for the bundle" + }, + "platformName": { + "type": "string", + "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")" + }, + "sdkName": { + "type": "string", + "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")" + } + }, + "type": "object", + "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string", + "description": "Value is the CPE string identifier." + }, + "source": { + "type": "string", + "description": "Source is the source where this CPE was obtained or generated from." + } + }, + "type": "object", + "required": [ + "cpe" + ], + "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases." + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, + "Descriptor": { + "properties": { + "name": { + "type": "string", + "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")." + }, + "version": { + "type": "string", + "description": "Version is the version of the tool that generated this SBOM." + }, + "configuration": { + "description": "Configuration contains the tool configuration used during SBOM generation." + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation." + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array", + "description": "Artifacts is the list of packages discovered and placed into the catalog" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array", + "description": "note: must have omitempty" + }, + "source": { + "$ref": "#/$defs/Source", + "description": "Source represents the original object that was cataloged" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease", + "description": "Distro represents the Linux distribution that was detected from the source" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor", + "description": "Descriptor is a block containing self-describing information about syft" + }, + "schema": { + "$ref": "#/$defs/Schema", + "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "type": { + "type": "string", + "description": "Type is type of entry could be package or project for internal refs" + }, + "executables": { + "additionalProperties": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing." + }, + "appCpe": { + "type": "string", + "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages." + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for this file within the SBOM." + }, + "location": { + "$ref": "#/$defs/Coordinates", + "description": "Location is the file path and layer information where this file was found." + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry", + "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type." + }, + "contents": { + "type": "string", + "description": "Contents is the file contents for small files." + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains cryptographic hashes of the file contents." + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array", + "description": "Licenses contains license information discovered within this file." + }, + "executable": { + "$ref": "#/$defs/Executable", + "description": "Executable contains executable metadata if this file is a binary." + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Unknowns contains unknown fields for forward compatibility." + } + }, + "type": "object", + "required": [ + "id", + "location" + ], + "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages." + }, + "FileLicense": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or text as found in the file." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence", + "description": "Evidence contains supporting evidence for this license detection." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ], + "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression." + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer", + "description": "Confidence is the confidence score for this license detection (0-100)." + }, + "offset": { + "type": "integer", + "description": "Offset is the byte offset where the license text starts in the file." + }, + "extent": { + "type": "integer", + "description": "Extent is the length of the license text in bytes." + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ], + "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level." + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer", + "description": "Mode is the Unix file permission mode in octal format." + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")." + }, + "linkDestination": { + "type": "string", + "description": "LinkDestination is the target path for symbolic links." + }, + "userID": { + "type": "integer", + "description": "UserID is the file owner user ID." + }, + "groupID": { + "type": "integer", + "description": "GroupID is the file owner group ID." + }, + "mimeType": { + "type": "string", + "description": "MIMEType is the MIME type of the file contents." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ], + "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file." + }, + "GgufFileHeader": { + "properties": { + "ggufVersion": { + "type": "integer", + "description": "GGUFVersion is the GGUF format version (e.g., 3)" + }, + "fileSize": { + "type": "integer", + "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")" + }, + "quantization": { + "type": "string", + "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")" + }, + "parameters": { + "type": "integer", + "description": "Parameters is the number of model parameters (if present in header)" + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensors in the model" + }, + "header": { + "type": "object", + "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication." + }, + "metadataHash": { + "type": "string", + "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames." + }, + "parts": { + "items": { + "$ref": "#/$defs/GgufFileHeader" + }, + "type": "array", + "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)." + } + }, + "type": "object", + "required": [ + "ggufVersion", + "tensorCount" + ], + "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file." + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field." + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptBunLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of runtime dependencies and their version specifiers" + }, + "optionalDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "OptionalDependencies is a map of optional dependencies and their version specifiers" + }, + "peerDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "PeerDependencies is a map of peer dependencies and their version specifiers" + }, + "bin": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Bin is a map of binary names to the paths they are installed to" + }, + "os": { + "type": "string", + "description": "OS is the operating system constraint for the package (e.g. \"darwin\")" + }, + "cpu": { + "type": "string", + "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies", + "optionalDependencies", + "peerDependencies", + "bin", + "os", + "cpu" + ], + "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\"" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptPnpmLockEntry": { + "properties": { + "resolution": { + "$ref": "#/$defs/PnpmLockResolution", + "description": "Resolution is the resolution information for the package" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolution", + "dependencies" + ], + "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or expression as found." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URLs are URLs where license text or information can be found." + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array", + "description": "Locations are file locations where this license was discovered." + }, + "contents": { + "type": "string", + "description": "Contents is the full license text content." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ], + "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations." + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string", + "description": "PrettyName is a human-readable operating system name with version." + }, + "name": { + "type": "string", + "description": "Name is the operating system name without version information." + }, + "id": { + "type": "string", + "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")." + }, + "idLike": { + "$ref": "#/$defs/IDLikes", + "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from." + }, + "version": { + "type": "string", + "description": "Version is the operating system version including codename if available." + }, + "versionID": { + "type": "string", + "description": "VersionID is the operating system version number or identifier." + }, + "versionCodename": { + "type": "string", + "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")." + }, + "buildID": { + "type": "string", + "description": "BuildID is a build identifier for the operating system." + }, + "imageID": { + "type": "string", + "description": "ImageID is an identifier for container or cloud images." + }, + "imageVersion": { + "type": "string", + "description": "ImageVersion is the version for container or cloud images." + }, + "variant": { + "type": "string", + "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")." + }, + "variantID": { + "type": "string", + "description": "VariantID is the lower-case operating system variant identifier." + }, + "homeURL": { + "type": "string", + "description": "HomeURL is the homepage URL for the operating system." + }, + "supportURL": { + "type": "string", + "description": "SupportURL is the support or help URL for the operating system." + }, + "bugReportURL": { + "type": "string", + "description": "BugReportURL is the bug reporting URL for the operating system." + }, + "privacyPolicyURL": { + "type": "string", + "description": "PrivacyPolicyURL is the privacy policy URL for the operating system." + }, + "cpeName": { + "type": "string", + "description": "CPEName is the Common Platform Enumeration name for the operating system." + }, + "supportEnd": { + "type": "string", + "description": "SupportEnd is the end of support date or version identifier." + }, + "extendedSupport": { + "type": "boolean", + "description": "ExtendedSupport indicates whether extended security or support is available." + } + }, + "type": "object", + "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files." + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Arbitrary key-value pairs that can be used to annotate a location" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/AppleAppBundleEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GgufFileHeader" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptBunLockEntry" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptPnpmLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PnpmLockResolution": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "integrity" + ], + "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmFileEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the file download URL" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest is the hash digest of the file hosted at the URL" + } + }, + "type": "object", + "required": [ + "url", + "digest" + ] + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests (for the base package without extras)" + }, + "marker": { + "type": "string", + "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment" + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications for the base package (without extras)" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPdmLockExtraVariant" + }, + "type": "array", + "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)" + } + }, + "type": "object", + "required": [ + "summary", + "files" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPdmLockExtraVariant": { + "properties": { + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependencies specific to this extras variant" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files specific to this variant (only populated if different from base)" + }, + "marker": { + "type": "string", + "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")" + } + }, + "type": "object", + "required": [ + "extras" + ], + "description": "PythonPdmLockExtraVariant represents a specific extras combination variant within a PDM lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string", + "description": "Parent is the ID of the parent artifact in this relationship." + }, + "child": { + "type": "string", + "description": "Child is the ID of the child artifact in this relationship." + }, + "type": { + "type": "string", + "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")." + }, + "metadata": { + "description": "Metadata contains additional relationship-specific metadata." + } + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ], + "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package." + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "Schema": { + "properties": { + "version": { + "type": "string", + "description": "Version is the JSON schema version for this document format." + }, + "url": { + "type": "string", + "description": "URL is the URL to the JSON schema definition document." + } + }, + "type": "object", + "required": [ + "version", + "url" + ], + "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format." + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for the analyzed source artifact." + }, + "name": { + "type": "string", + "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)." + }, + "version": { + "type": "string", + "description": "Version is the version of the analyzed artifact (e.g., image tag)." + }, + "supplier": { + "type": "string", + "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance." + }, + "type": { + "type": "string", + "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")." + }, + "metadata": { + "description": "Metadata contains additional source-specific metadata." + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 98796e99c..71e1dd2ff 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.1.6/document", + "$id": "anchore.io/schema/syft/json/16.1.7/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -249,6 +249,67 @@ ], "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." }, + "AppleAppBundleEntry": { + "properties": { + "bundleIdentifier": { + "type": "string", + "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")" + }, + "name": { + "type": "string", + "description": "Name is the short name of the bundle" + }, + "displayName": { + "type": "string", + "description": "DisplayName is the user-facing name of the bundle" + }, + "executable": { + "type": "string", + "description": "Executable is the name of the executable within the bundle" + }, + "shortVersion": { + "type": "string", + "description": "ShortVersion is the release (marketing) version of the bundle" + }, + "version": { + "type": "string", + "description": "Version is the build version of the bundle, which often differs from the short version" + }, + "packageType": { + "type": "string", + "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)" + }, + "supportedPlatforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")" + }, + "minimumSystemVersion": { + "type": "string", + "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle" + }, + "minimumOSVersion": { + "type": "string", + "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)" + }, + "copyright": { + "type": "string", + "description": "Copyright is the human-readable copyright notice for the bundle" + }, + "platformName": { + "type": "string", + "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")" + }, + "sdkName": { + "type": "string", + "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")" + } + }, + "type": "object", + "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files." + }, "BinarySignature": { "properties": { "matches": { @@ -2674,6 +2735,9 @@ { "$ref": "#/$defs/ApkDbEntry" }, + { + "$ref": "#/$defs/AppleAppBundleEntry" + }, { "$ref": "#/$defs/BinarySignature" }, diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index f42654945..d18075450 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -31,6 +31,7 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.GolangModuleEntry{}, pkg.GolangSourceEntry{}, pkg.HomebrewFormula{}, + pkg.AppleAppBundleEntry{}, pkg.HackageStackYamlLockEntry{}, pkg.HackageStackYamlEntry{}, pkg.LinuxKernel{}, diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go index a26c1d74b..267703ab0 100644 --- a/syft/format/internal/spdxutil/helpers/source_info.go +++ b/syft/format/internal/spdxutil/helpers/source_info.go @@ -80,6 +80,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from found wordpress plugin PHP source files" case pkg.HomebrewPkg: answer = "acquired package info from Homebrew formula" + case pkg.AppleAppBundlePkg: + answer = "acquired package info from Apple application bundle Info.plist" case pkg.TerraformPkg: answer = "acquired package info from Terraform dependency lock file" case pkg.ModelPkg: diff --git a/syft/format/internal/spdxutil/helpers/source_info_test.go b/syft/format/internal/spdxutil/helpers/source_info_test.go index 13338f8c4..e16b6f974 100644 --- a/syft/format/internal/spdxutil/helpers/source_info_test.go +++ b/syft/format/internal/spdxutil/helpers/source_info_test.go @@ -343,6 +343,14 @@ func Test_SourceInfo(t *testing.T) { "acquired package info from Homebrew formula", }, }, + { + input: pkg.Package{ + Type: pkg.AppleAppBundlePkg, + }, + expected: []string{ + "acquired package info from Apple application bundle Info.plist", + }, + }, { input: pkg.Package{ Type: pkg.TerraformPkg, diff --git a/syft/pkg/apple.go b/syft/pkg/apple.go new file mode 100644 index 000000000..63fd01c48 --- /dev/null +++ b/syft/pkg/apple.go @@ -0,0 +1,44 @@ +package pkg + +// AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files. +// These bundles share the same plist structure across macOS, iOS, watchOS, visionOS, and others. +type AppleAppBundleEntry struct { + // BundleIdentifier is the unique identifier for the bundle (e.g. "com.apple.Safari") + BundleIdentifier string `json:"bundleIdentifier,omitempty"` + + // Name is the short name of the bundle + Name string `json:"name,omitempty"` + + // DisplayName is the user-facing name of the bundle + DisplayName string `json:"displayName,omitempty"` + + // Executable is the name of the executable within the bundle + Executable string `json:"executable,omitempty"` + + // ShortVersion is the release (marketing) version of the bundle + ShortVersion string `json:"shortVersion,omitempty"` + + // Version is the build version of the bundle, which often differs from the short version + Version string `json:"version,omitempty"` + + // PackageType is the four-letter type code (e.g. "APPL" for apps, "FMWK" for frameworks, "BNDL" for bundles) + PackageType string `json:"packageType,omitempty"` + + // SupportedPlatforms lists the platforms the bundle targets (e.g. "MacOSX", "iPhoneOS") + SupportedPlatforms []string `json:"supportedPlatforms,omitempty"` + + // MinimumSystemVersion is the minimum macOS version required to run the bundle + MinimumSystemVersion string `json:"minimumSystemVersion,omitempty"` + + // MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS) + MinimumOSVersion string `json:"minimumOSVersion,omitempty"` + + // Copyright is the human-readable copyright notice for the bundle + Copyright string `json:"copyright,omitempty"` + + // PlatformName is the platform name of the SDK used to build the bundle (e.g. "macosx") + PlatformName string `json:"platformName,omitempty"` + + // SDKName is the name of the SDK used to build the bundle (e.g. "macosx14.0") + SDKName string `json:"sdkName,omitempty"` +} diff --git a/syft/pkg/cataloger/apple/capabilities.yaml b/syft/pkg/cataloger/apple/capabilities.yaml new file mode 100644 index 000000000..178a17283 --- /dev/null +++ b/syft/pkg/cataloger/apple/capabilities.yaml @@ -0,0 +1,42 @@ +# Cataloger capabilities. See ../README.md for documentation. + +catalogers: + - ecosystem: apple # MANUAL + name: apple-app-bundle-cataloger # AUTO-GENERATED + type: generic # AUTO-GENERATED + source: # AUTO-GENERATED + file: syft/pkg/cataloger/apple/cataloger.go + function: NewAppBundleCataloger + selectors: # AUTO-GENERATED + - apple + - directory + - image + - installed + - package + parsers: # AUTO-GENERATED structure + - function: parseInfoPlist + detector: # AUTO-GENERATED + method: glob # AUTO-GENERATED + criteria: # AUTO-GENERATED + - '**/*.app/Contents/Info.plist' + metadata_types: # AUTO-GENERATED + - pkg.AppleAppBundleEntry + package_types: # AUTO-GENERATED + - apple-app-bundle + json_schema_types: # AUTO-GENERATED + - AppleAppBundleEntry + capabilities: # MANUAL - preserved across regeneration + - name: license + default: false + - name: dependency.depth + default: [] + - name: dependency.edges + default: "" + - name: dependency.kinds + default: [] + - name: package_manager.files.listing + default: false + - name: package_manager.files.digests + default: false + - name: package_manager.package_integrity_hash + default: false diff --git a/syft/pkg/cataloger/apple/cataloger.go b/syft/pkg/cataloger/apple/cataloger.go new file mode 100644 index 000000000..c5ca61984 --- /dev/null +++ b/syft/pkg/cataloger/apple/cataloger.go @@ -0,0 +1,12 @@ +package apple + +import ( + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +// NewAppBundleCataloger returns a cataloger for Apple application bundles (.app), reading their Info.plist (CFBundle). +func NewAppBundleCataloger() pkg.Cataloger { + return generic.NewCataloger("apple-app-bundle-cataloger"). + WithParserByGlobs(parseInfoPlist, "**/*.app/Contents/Info.plist") +} diff --git a/syft/pkg/cataloger/apple/cataloger_test.go b/syft/pkg/cataloger/apple/cataloger_test.go new file mode 100644 index 000000000..6264e88f9 --- /dev/null +++ b/syft/pkg/cataloger/apple/cataloger_test.go @@ -0,0 +1,180 @@ +package apple + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func Test_AppleAppBundleCataloger_Globs(t *testing.T) { + pkgtest.NewCatalogTester(). + FromDirectory(t, "testdata/install-example"). + ExpectsResolverContentQueries([]string{ + "Applications/Slack.app/Contents/Info.plist", + }). + TestCataloger(t, NewAppBundleCataloger()) +} + +func Test_AppleAppBundleCataloger(t *testing.T) { + tests := []struct { + name string + fixture string + expected []pkg.Package + }{ + { + name: "xml plist (Slack)", + fixture: "testdata/install-example", + expected: []pkg.Package{ + { + Name: "Slack", + Version: "4.50.128", + Type: pkg.AppleAppBundlePkg, + FoundBy: "apple-app-bundle-cataloger", + Locations: file.NewLocationSet( + file.NewLocation("Applications/Slack.app/Contents/Info.plist").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + PURL: "", // no standard purl type for apple app bundles + Metadata: pkg.AppleAppBundleEntry{ + BundleIdentifier: "com.tinyspeck.slackmacgap", + Name: "Slack", + DisplayName: "Slack", + Executable: "Slack", + ShortVersion: "4.50.128", + Version: "450000128", + PackageType: "APPL", + MinimumSystemVersion: "12.0", + Copyright: "©2026 Slack Technologies LLC, a Salesforce company. All rights reserved.", + SDKName: "macosx15.5", + }, + }, + }, + }, + { + // platform/array fields; values are from a real Ghostty.app + name: "platform fields (Ghostty)", + fixture: "testdata/native-app-example", + expected: []pkg.Package{ + { + Name: "Ghostty", + Version: "1.3.1", + Type: pkg.AppleAppBundlePkg, + FoundBy: "apple-app-bundle-cataloger", + Locations: file.NewLocationSet( + file.NewLocation("Applications/Ghostty.app/Contents/Info.plist").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + PURL: "", + Metadata: pkg.AppleAppBundleEntry{ + BundleIdentifier: "com.mitchellh.ghostty", + Name: "Ghostty", + DisplayName: "Ghostty", + Executable: "ghostty", + ShortVersion: "1.3.1", + Version: "15212", + PackageType: "APPL", + SupportedPlatforms: []string{"MacOSX"}, + MinimumSystemVersion: "13.0", + PlatformName: "macosx", + SDKName: "macosx26.2", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkgtest.NewCatalogTester(). + FromDirectory(t, tt.fixture). + Expects(tt.expected, nil). + TestCataloger(t, NewAppBundleCataloger()) + }) + } +} + +func Test_parseInfoPlist(t *testing.T) { + tests := []struct { + name string + fixture string + wantName string + wantVersion string + wantBundleID string + wantNoPackage bool + wantParseErr bool + }{ + { + name: "name falls back to executable", + fixture: "testdata/parse-cases/name-from-executable.plist", + wantName: "Exec", + wantVersion: "1.0", + }, + { + name: "name falls back to bundle identifier", + fixture: "testdata/parse-cases/name-from-identifier.plist", + wantName: "com.example.app", + wantVersion: "1.0", + wantBundleID: "com.example.app", + }, + { + name: "version falls back to bundle version", + fixture: "testdata/parse-cases/version-from-bundle-version.plist", + wantName: "Name", + wantVersion: "42", + }, + { + name: "no package without a name", + fixture: "testdata/parse-cases/no-name.plist", + wantNoPackage: true, + }, + { + name: "no package without a version", + fixture: "testdata/parse-cases/no-version.plist", + wantNoPackage: true, + }, + { + name: "malformed plist returns an error", + fixture: "testdata/parse-cases/malformed.plist", + wantParseErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkgs, _, err := parseFixture(t, tt.fixture) + if tt.wantParseErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + if tt.wantNoPackage { + assert.Empty(t, pkgs) + return + } + + require.Len(t, pkgs, 1) + assert.Equal(t, tt.wantName, pkgs[0].Name) + assert.Equal(t, tt.wantVersion, pkgs[0].Version) + assert.Equal(t, pkg.AppleAppBundlePkg, pkgs[0].Type) + assert.Empty(t, pkgs[0].PURL) + meta, ok := pkgs[0].Metadata.(pkg.AppleAppBundleEntry) + require.True(t, ok) + assert.Equal(t, tt.wantBundleID, meta.BundleIdentifier) + }) + } +} + +func parseFixture(t *testing.T, path string) ([]pkg.Package, []artifact.Relationship, error) { + t.Helper() + f, err := os.Open(path) + require.NoError(t, err) + t.Cleanup(func() { _ = f.Close() }) + return parseInfoPlist(context.Background(), nil, nil, file.NewLocationReadCloser(file.NewLocation(path), f)) +} diff --git a/syft/pkg/cataloger/apple/package.go b/syft/pkg/cataloger/apple/package.go new file mode 100644 index 000000000..57f7a9798 --- /dev/null +++ b/syft/pkg/cataloger/apple/package.go @@ -0,0 +1,20 @@ +package apple + +import ( + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +func newAppBundlePackage(name, version string, metadata pkg.AppleAppBundleEntry, location file.Location) pkg.Package { + p := pkg.Package{ + Name: name, + Version: version, + Type: pkg.AppleAppBundlePkg, + // note: there is no standard purl type for Apple app bundles, so we intentionally leave the PURL empty. + Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Metadata: metadata, + } + + p.SetID() + return p +} diff --git a/syft/pkg/cataloger/apple/parse_info_plist.go b/syft/pkg/cataloger/apple/parse_info_plist.go new file mode 100644 index 000000000..a0e2d7ddf --- /dev/null +++ b/syft/pkg/cataloger/apple/parse_info_plist.go @@ -0,0 +1,91 @@ +package apple + +import ( + "context" + "fmt" + "io" + + "howett.net/plist" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +// infoPlist captures the raw CFBundle keys read from an Info.plist. It is kept separate from the public +// pkg.AppleAppBundleEntry so the parser can read fields without committing to expressing all of them. +type infoPlist struct { + CFBundleIdentifier string `plist:"CFBundleIdentifier"` + CFBundleName string `plist:"CFBundleName"` + CFBundleDisplayName string `plist:"CFBundleDisplayName"` + CFBundleExecutable string `plist:"CFBundleExecutable"` + CFBundleShortVersionString string `plist:"CFBundleShortVersionString"` + CFBundleVersion string `plist:"CFBundleVersion"` + CFBundlePackageType string `plist:"CFBundlePackageType"` + CFBundleSupportedPlatforms []string `plist:"CFBundleSupportedPlatforms"` + LSMinimumSystemVersion string `plist:"LSMinimumSystemVersion"` + MinimumOSVersion string `plist:"MinimumOSVersion"` + NSHumanReadableCopyright string `plist:"NSHumanReadableCopyright"` + DTPlatformName string `plist:"DTPlatformName"` + DTSDKName string `plist:"DTSDKName"` +} + +func parseInfoPlist(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + // 5MB cap matches the convention in syft/linux; Info.plist files are far smaller + data, err := io.ReadAll(io.LimitReader(reader, 5*1024*1024)) + if err != nil { + return nil, nil, fmt.Errorf("unable to read file: %w", err) + } + + var info infoPlist + if _, err := plist.Unmarshal(data, &info); err != nil { + return nil, nil, fmt.Errorf("unable to parse plist: %w", err) + } + + name := info.CFBundleDisplayName + if name == "" { + name = info.CFBundleName + } + if name == "" { + name = info.CFBundleExecutable + } + if name == "" { + // last-resort name so identifiable bundles aren't dropped (e.g. com.apple.Safari) + name = info.CFBundleIdentifier + } + + version := info.CFBundleShortVersionString + if version == "" { + version = info.CFBundleVersion + } + + if name == "" || version == "" { + return nil, nil, nil + } + + pkgs := []pkg.Package{ + newAppBundlePackage(name, version, info.toEntry(), reader.Location), + } + + return pkgs, nil, nil +} + +// toEntry maps the parsed plist into the public metadata type. +func (i infoPlist) toEntry() pkg.AppleAppBundleEntry { + return pkg.AppleAppBundleEntry{ + BundleIdentifier: i.CFBundleIdentifier, + Name: i.CFBundleName, + DisplayName: i.CFBundleDisplayName, + Executable: i.CFBundleExecutable, + ShortVersion: i.CFBundleShortVersionString, + Version: i.CFBundleVersion, + PackageType: i.CFBundlePackageType, + SupportedPlatforms: i.CFBundleSupportedPlatforms, + MinimumSystemVersion: i.LSMinimumSystemVersion, + MinimumOSVersion: i.MinimumOSVersion, + Copyright: i.NSHumanReadableCopyright, + PlatformName: i.DTPlatformName, + SDKName: i.DTSDKName, + } +} diff --git a/syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist b/syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist new file mode 100644 index 000000000..2e5c78a6b --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDisplayName + Slack + CFBundleName + Slack + CFBundleExecutable + Slack + CFBundleIdentifier + com.tinyspeck.slackmacgap + CFBundleShortVersionString + 4.50.128 + CFBundleVersion + 450000128 + CFBundlePackageType + APPL + LSMinimumSystemVersion + 12.0 + NSHumanReadableCopyright + ©2026 Slack Technologies LLC, a Salesforce company. All rights reserved. + DTSDKName + macosx15.5 + + diff --git a/syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist b/syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist new file mode 100644 index 000000000..70ad52af5 --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleName + Ghostty + CFBundleDisplayName + Ghostty + CFBundleExecutable + ghostty + CFBundleIdentifier + com.mitchellh.ghostty + CFBundleShortVersionString + 1.3.1 + CFBundleVersion + 15212 + CFBundlePackageType + APPL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + 13.0 + DTPlatformName + macosx + DTSDKName + macosx26.2 + + diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist new file mode 100644 index 000000000..06dd84d16 --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist @@ -0,0 +1 @@ +this is not a valid plist diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist new file mode 100644 index 000000000..eda8f5d08 --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist @@ -0,0 +1,10 @@ + + + + + CFBundleExecutable + Exec + CFBundleShortVersionString + 1.0 + + diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist new file mode 100644 index 000000000..d68bc8a46 --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist @@ -0,0 +1,10 @@ + + + + + CFBundleIdentifier + com.example.app + CFBundleShortVersionString + 1.0 + + diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist new file mode 100644 index 000000000..a300be381 --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist @@ -0,0 +1,8 @@ + + + + + CFBundleShortVersionString + 1.0 + + diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist new file mode 100644 index 000000000..e4228731d --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist @@ -0,0 +1,8 @@ + + + + + CFBundleName + Name + + diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist new file mode 100644 index 000000000..67076c0e4 --- /dev/null +++ b/syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist @@ -0,0 +1,10 @@ + + + + + CFBundleName + Name + CFBundleVersion + 42 + + diff --git a/syft/pkg/type.go b/syft/pkg/type.go index d4abe75a6..b7155bdb6 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -54,6 +54,7 @@ const ( TerraformPkg Type = "terraform" WordpressPluginPkg Type = "wordpress-plugin" HomebrewPkg Type = "homebrew" + AppleAppBundlePkg Type = "apple-app-bundle" ModelPkg Type = "model" ) @@ -99,6 +100,7 @@ var AllPkgs = []Type{ TerraformPkg, WordpressPluginPkg, HomebrewPkg, + AppleAppBundlePkg, ModelPkg, } diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 580ea5c79..0e4201706 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -160,8 +160,9 @@ func TestTypeFromPURL(t *testing.T) { expectedTypes.Remove(string(HomebrewPkg)) expectedTypes.Remove(string(TerraformPkg)) expectedTypes.Remove(string(GraalVMNativeImagePkg)) - expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently - expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package + expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently + expectedTypes.Remove(string(AppleAppBundlePkg)) // no standard purl type for apple app bundles + expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package for _, test := range tests { t.Run(string(test.expected), func(t *testing.T) { From 656a4d46d7418c1326c4d3648392eb024bb7b461 Mon Sep 17 00:00:00 2001 From: gab <32674884+gabetrau@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:11:33 -0500 Subject: [PATCH 037/168] Vcpkg Cataloger (#4081) * Vcpkg cataloger for vcpkg "Manifest Mode" Find and parse vcpkg-lock.json to get HEAD commit hash Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * just use local vcpkg git repo if it exists, clone it if it doesn't Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * Config opt for git remote clones for vcpkg and README update Signed-off-by: Gabriel Rau * Look in vcpkg cache git repo for custom git repos Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * add triplet to metadata and support overlay-ports from config file Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * Add PURL to packages (not sure if this is correct) Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * flatten structs in pkg module and move vcpkg structs to resolver Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * account for overriden versions in toplevel manifest Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * generate json schema for vcpkg metadata Signed-off-by: Gabriel Rau * test for basic vcpkg project dependencies for vcpkg registry to be pulled in add tree hashes and use correct git hash in builtin-baseline for helloworld test vcpkg-registry for testing that uses object hashes from syft repo fix broken tests Signed-off-by: Gabriel Rau * formatting Signed-off-by: Gabriel Rau * fix static-analysis violations Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * fix integration test failure Signed-off-by: Gabriel Rau * remove uneeded files from vcpkg test fixture and use custom registry Signed-off-by: Gabriel Rau * change vcpkg registry to anchore one Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * purl spec based on open PR Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * generate-json-schema Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman * rebased and generate json schema 16.0.40 Signed-off-by: Gabriel Rau * address low hanging fruit Signed-off-by: Alex Goodman * handle additional comments Signed-off-by: Alex Goodman * migrate to testdata Signed-off-by: Alex Goodman * improve docs and testing Signed-off-by: Alex Goodman * fix static analysis Signed-off-by: Alex Goodman * remove license from pkg metadata Signed-off-by: Alex Goodman * fix capabilities claim Signed-off-by: Alex Goodman --------- Signed-off-by: Gabriel Rau Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- cmd/syft/internal/options/catalog.go | 6 + cmd/syft/internal/options/cpp.go | 30 + .../test/integration/catalog_packages_test.go | 2 + internal/capabilities/appconfig.yaml | 2 + internal/constants.go | 4 +- internal/packagemetadata/generated.go | 1 + internal/packagemetadata/names.go | 1 + internal/task/package_tasks.go | 9 + schema/json/schema-16.1.8.json | 4505 +++++++++++++++++ schema/json/schema-latest.json | 85 +- syft/cataloging/pkgcataloging/config.go | 8 + .../spdxutil/helpers/originator_supplier.go | 5 + .../helpers/originator_supplier_test.go | 1 + .../internal/spdxutil/helpers/source_info.go | 2 + .../spdxutil/helpers/source_info_test.go | 8 + syft/pkg/cataloger/cpp/capabilities.yaml | 52 + syft/pkg/cataloger/cpp/cataloger.go | 32 + syft/pkg/cataloger/cpp/cataloger_test.go | 4 +- syft/pkg/cataloger/cpp/config.go | 19 + .../cataloger/cpp/internal/vcpkg/resolver.go | 697 +++ syft/pkg/cataloger/cpp/package.go | 68 +- syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go | 182 + .../cataloger/cpp/parse_vcpkgmanifest_test.go | 244 + syft/pkg/cataloger/cpp/testdata/.gitignore | 2 + syft/pkg/cataloger/cpp/testdata/README.md | 15 +- .../vcpkg_installed/vcpkg/vcpkg-lock.json | 1 + .../somewhere/src/vcpkg-configuration.json | 1 + .../glob-paths/somewhere/src/vcpkg.json | 1 + .../testdata/vcpkg/helloworld/helloworld.cpp | 7 + .../vcpkg/helloworld/vcpkg-configuration.json | 7 + .../cpp/testdata/vcpkg/helloworld/vcpkg.json | 10 + syft/pkg/type.go | 6 + syft/pkg/type_test.go | 1 + syft/pkg/vcpkg.go | 35 + 34 files changed, 6042 insertions(+), 11 deletions(-) create mode 100644 cmd/syft/internal/options/cpp.go create mode 100644 schema/json/schema-16.1.8.json create mode 100644 syft/pkg/cataloger/cpp/config.go create mode 100644 syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go create mode 100644 syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go create mode 100644 syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go create mode 100644 syft/pkg/cataloger/cpp/testdata/.gitignore create mode 100644 syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json create mode 100644 syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json create mode 100644 syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json create mode 100644 syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp create mode 100644 syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json create mode 100644 syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json create mode 100644 syft/pkg/vcpkg.go diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 1b74df358..4ffbfad56 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -20,6 +20,7 @@ import ( "github.com/anchore/syft/syft/file/cataloger/executable" "github.com/anchore/syft/syft/file/cataloger/filecontent" "github.com/anchore/syft/syft/pkg/cataloger/binary" + "github.com/anchore/syft/syft/pkg/cataloger/cpp" "github.com/anchore/syft/syft/pkg/cataloger/dotnet" "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/java" @@ -44,6 +45,7 @@ type Catalog struct { Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"` // ecosystem-specific cataloger configuration + Cpp cppConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"` Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"` Java javaConfig `yaml:"java" json:"java" mapstructure:"java"` @@ -80,6 +82,7 @@ func DefaultCatalog() Catalog { JavaScript: defaultJavaScriptConfig(), Python: defaultPythonConfig(), Nix: defaultNixConfig(), + Cpp: defaultCppConfig(), Dotnet: defaultDotnetConfig(), Golang: defaultGolangConfig(), Java: defaultJavaConfig(), @@ -172,6 +175,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { } return pkgcataloging.Config{ Binary: binary.DefaultClassifierCatalogerConfig(), + Cpp: cpp.DefaultCatalogerConfig(). + WithVcpkgAllowGitClone(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Cpp, task.Vcpkg), cfg.Cpp.VcpkgAllowGitClone)), Dotnet: dotnet.DefaultCatalogerConfig(). WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL). WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL). @@ -301,6 +306,7 @@ var publicisedEnrichmentOptions = []string{ task.Java, task.JavaScript, task.Python, + task.Vcpkg, } func enrichmentEnabled(enrichDirectives []string, features ...string) *bool { diff --git a/cmd/syft/internal/options/cpp.go b/cmd/syft/internal/options/cpp.go new file mode 100644 index 000000000..84a45047e --- /dev/null +++ b/cmd/syft/internal/options/cpp.go @@ -0,0 +1,30 @@ +package options + +import ( + "github.com/anchore/clio" + "github.com/anchore/syft/syft/pkg/cataloger/cpp" +) + +type cppConfig struct { + VcpkgAllowGitClone *bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"` +} + +func defaultCppConfig() cppConfig { + // reference the cataloger default so capability generation can associate this config with the cpp + // ecosystem (it discovers ecosystem configs by their cataloger import). the value itself stays nil: + // nil defaults to false (no network), and leaving it unset lets --enrich opt in. cloning requires a + // network connection, which must be opt-in. + _ = cpp.DefaultCatalogerConfig() + return cppConfig{ + VcpkgAllowGitClone: nil, + } +} + +var _ interface { + clio.FieldDescriber +} = (*cppConfig)(nil) + +func (o *cppConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&o.VcpkgAllowGitClone, `enables Syft to use clone remote repositories for vcpkg custom git registries. + (also useful if the builtin vcpkg registry is not cloned locally)`) +} diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go index 9abbb338d..5816c1999 100644 --- a/cmd/syft/internal/test/integration/catalog_packages_test.go +++ b/cmd/syft/internal/test/integration/catalog_packages_test.go @@ -89,6 +89,7 @@ func TestPkgCoverageImage(t *testing.T) { definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead definedPkgs.Remove(string(pkg.CondaPkg)) definedPkgs.Remove(string(pkg.ModelPkg)) + definedPkgs.Remove(string(pkg.VcpkgPkg)) definedPkgs.Remove(string(pkg.AppleAppBundlePkg)) var cases []testCase @@ -165,6 +166,7 @@ func TestPkgCoverageDirectory(t *testing.T) { definedPkgs.Remove(string(pkg.CondaPkg)) definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages definedPkgs.Remove(string(pkg.ModelPkg)) + definedPkgs.Remove(string(pkg.VcpkgPkg)) definedPkgs.Remove(string(pkg.AppleAppBundlePkg)) // for directory scans we should not expect to see any of the following package types diff --git a/internal/capabilities/appconfig.yaml b/internal/capabilities/appconfig.yaml index b42a8ee29..e073dd32c 100644 --- a/internal/capabilities/appconfig.yaml +++ b/internal/capabilities/appconfig.yaml @@ -2,6 +2,8 @@ # This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate. application: # AUTO-GENERATED - application-level config keys + - key: cpp.vcpkg-allow-git-clone + description: enables Syft to use clone remote repositories for vcpkg custom git registries. (also useful if the builtin vcpkg registry is not cloned locally) - key: dotnet.dep-packages-must-claim-dll description: only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL. - key: dotnet.dep-packages-must-have-dll diff --git a/internal/constants.go b/internal/constants.go index 3f27e106c..0efb094e2 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,7 +3,7 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.1.7" + JSONSchemaVersion = "16.1.8" // Changelog // 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). @@ -14,5 +14,5 @@ const ( // 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support // 16.1.6 - add Dependencies to ElixirMixLockEntry metadata // 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger - + // 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support ) diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go index 1d39a8288..78fe51347 100644 --- a/internal/packagemetadata/generated.go +++ b/internal/packagemetadata/generated.go @@ -72,6 +72,7 @@ func AllTypes() []any { pkg.SwiftPackageManagerResolvedEntry{}, pkg.SwiplPackEntry{}, pkg.TerraformLockProviderEntry{}, + pkg.VcpkgManifest{}, pkg.WordpressPluginEntry{}, pkg.YarnLockEntry{}, } diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go index 408667974..e4370a897 100644 --- a/internal/packagemetadata/names.go +++ b/internal/packagemetadata/names.go @@ -130,6 +130,7 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"), jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"), jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"), + jsonNames(pkg.VcpkgManifest{}, "vcpkg-manifest"), ) func expandLegacyNameVariants(names ...string) []string { diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index 71e9e3af6..fa6e3a950 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -57,6 +57,10 @@ const ( // Python ecosystem labels Python = "python" + + // C/C++ ecosystem labels + Cpp = "cpp" + Vcpkg = "vcpkg" ) //nolint:funlen @@ -83,6 +87,11 @@ func DefaultPackageTaskFactories() Factories { // language-specific package declared catalogers /////////////////////////////////////////////////////////////////////////// newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"), + newPackageTaskFactory( + func(cfg CatalogingFactoryConfig) pkg.Cataloger { + return cpp.NewVcpkgManifestCataloger(cfg.PackagesConfig.Cpp) + }, + pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Cpp, Vcpkg), newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"), newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"), newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"), diff --git a/schema/json/schema-16.1.8.json b/schema/json/schema-16.1.8.json new file mode 100644 index 000000000..1fc811850 --- /dev/null +++ b/schema/json/schema-16.1.8.json @@ -0,0 +1,4505 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.1.8/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object", + "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman." + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "AppleAppBundleEntry": { + "properties": { + "bundleIdentifier": { + "type": "string", + "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")" + }, + "name": { + "type": "string", + "description": "Name is the short name of the bundle" + }, + "displayName": { + "type": "string", + "description": "DisplayName is the user-facing name of the bundle" + }, + "executable": { + "type": "string", + "description": "Executable is the name of the executable within the bundle" + }, + "shortVersion": { + "type": "string", + "description": "ShortVersion is the release (marketing) version of the bundle" + }, + "version": { + "type": "string", + "description": "Version is the build version of the bundle, which often differs from the short version" + }, + "packageType": { + "type": "string", + "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)" + }, + "supportedPlatforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")" + }, + "minimumSystemVersion": { + "type": "string", + "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle" + }, + "minimumOSVersion": { + "type": "string", + "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)" + }, + "copyright": { + "type": "string", + "description": "Copyright is the human-readable copyright notice for the bundle" + }, + "platformName": { + "type": "string", + "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")" + }, + "sdkName": { + "type": "string", + "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")" + } + }, + "type": "object", + "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string", + "description": "Value is the CPE string identifier." + }, + "source": { + "type": "string", + "description": "Source is the source where this CPE was obtained or generated from." + } + }, + "type": "object", + "required": [ + "cpe" + ], + "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases." + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, + "Descriptor": { + "properties": { + "name": { + "type": "string", + "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")." + }, + "version": { + "type": "string", + "description": "Version is the version of the tool that generated this SBOM." + }, + "configuration": { + "description": "Configuration contains the tool configuration used during SBOM generation." + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation." + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array", + "description": "Artifacts is the list of packages discovered and placed into the catalog" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array", + "description": "note: must have omitempty" + }, + "source": { + "$ref": "#/$defs/Source", + "description": "Source represents the original object that was cataloged" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease", + "description": "Distro represents the Linux distribution that was detected from the source" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor", + "description": "Descriptor is a block containing self-describing information about syft" + }, + "schema": { + "$ref": "#/$defs/Schema", + "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "type": { + "type": "string", + "description": "Type is type of entry could be package or project for internal refs" + }, + "executables": { + "additionalProperties": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing." + }, + "appCpe": { + "type": "string", + "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages." + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for this file within the SBOM." + }, + "location": { + "$ref": "#/$defs/Coordinates", + "description": "Location is the file path and layer information where this file was found." + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry", + "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type." + }, + "contents": { + "type": "string", + "description": "Contents is the file contents for small files." + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains cryptographic hashes of the file contents." + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array", + "description": "Licenses contains license information discovered within this file." + }, + "executable": { + "$ref": "#/$defs/Executable", + "description": "Executable contains executable metadata if this file is a binary." + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Unknowns contains unknown fields for forward compatibility." + } + }, + "type": "object", + "required": [ + "id", + "location" + ], + "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages." + }, + "FileLicense": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or text as found in the file." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence", + "description": "Evidence contains supporting evidence for this license detection." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ], + "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression." + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer", + "description": "Confidence is the confidence score for this license detection (0-100)." + }, + "offset": { + "type": "integer", + "description": "Offset is the byte offset where the license text starts in the file." + }, + "extent": { + "type": "integer", + "description": "Extent is the length of the license text in bytes." + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ], + "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level." + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer", + "description": "Mode is the Unix file permission mode in octal format." + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")." + }, + "linkDestination": { + "type": "string", + "description": "LinkDestination is the target path for symbolic links." + }, + "userID": { + "type": "integer", + "description": "UserID is the file owner user ID." + }, + "groupID": { + "type": "integer", + "description": "GroupID is the file owner group ID." + }, + "mimeType": { + "type": "string", + "description": "MIMEType is the MIME type of the file contents." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ], + "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file." + }, + "GgufFileHeader": { + "properties": { + "ggufVersion": { + "type": "integer", + "description": "GGUFVersion is the GGUF format version (e.g., 3)" + }, + "fileSize": { + "type": "integer", + "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")" + }, + "quantization": { + "type": "string", + "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")" + }, + "parameters": { + "type": "integer", + "description": "Parameters is the number of model parameters (if present in header)" + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensors in the model" + }, + "header": { + "type": "object", + "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication." + }, + "metadataHash": { + "type": "string", + "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames." + }, + "parts": { + "items": { + "$ref": "#/$defs/GgufFileHeader" + }, + "type": "array", + "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)." + } + }, + "type": "object", + "required": [ + "ggufVersion", + "tensorCount" + ], + "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file." + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field." + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptBunLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of runtime dependencies and their version specifiers" + }, + "optionalDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "OptionalDependencies is a map of optional dependencies and their version specifiers" + }, + "peerDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "PeerDependencies is a map of peer dependencies and their version specifiers" + }, + "bin": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Bin is a map of binary names to the paths they are installed to" + }, + "os": { + "type": "string", + "description": "OS is the operating system constraint for the package (e.g. \"darwin\")" + }, + "cpu": { + "type": "string", + "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies", + "optionalDependencies", + "peerDependencies", + "bin", + "os", + "cpu" + ], + "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\"" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptPnpmLockEntry": { + "properties": { + "resolution": { + "$ref": "#/$defs/PnpmLockResolution", + "description": "Resolution is the resolution information for the package" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolution", + "dependencies" + ], + "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or expression as found." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URLs are URLs where license text or information can be found." + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array", + "description": "Locations are file locations where this license was discovered." + }, + "contents": { + "type": "string", + "description": "Contents is the full license text content." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ], + "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations." + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string", + "description": "PrettyName is a human-readable operating system name with version." + }, + "name": { + "type": "string", + "description": "Name is the operating system name without version information." + }, + "id": { + "type": "string", + "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")." + }, + "idLike": { + "$ref": "#/$defs/IDLikes", + "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from." + }, + "version": { + "type": "string", + "description": "Version is the operating system version including codename if available." + }, + "versionID": { + "type": "string", + "description": "VersionID is the operating system version number or identifier." + }, + "versionCodename": { + "type": "string", + "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")." + }, + "buildID": { + "type": "string", + "description": "BuildID is a build identifier for the operating system." + }, + "imageID": { + "type": "string", + "description": "ImageID is an identifier for container or cloud images." + }, + "imageVersion": { + "type": "string", + "description": "ImageVersion is the version for container or cloud images." + }, + "variant": { + "type": "string", + "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")." + }, + "variantID": { + "type": "string", + "description": "VariantID is the lower-case operating system variant identifier." + }, + "homeURL": { + "type": "string", + "description": "HomeURL is the homepage URL for the operating system." + }, + "supportURL": { + "type": "string", + "description": "SupportURL is the support or help URL for the operating system." + }, + "bugReportURL": { + "type": "string", + "description": "BugReportURL is the bug reporting URL for the operating system." + }, + "privacyPolicyURL": { + "type": "string", + "description": "PrivacyPolicyURL is the privacy policy URL for the operating system." + }, + "cpeName": { + "type": "string", + "description": "CPEName is the Common Platform Enumeration name for the operating system." + }, + "supportEnd": { + "type": "string", + "description": "SupportEnd is the end of support date or version identifier." + }, + "extendedSupport": { + "type": "boolean", + "description": "ExtendedSupport indicates whether extended security or support is available." + } + }, + "type": "object", + "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files." + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Arbitrary key-value pairs that can be used to annotate a location" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/AppleAppBundleEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GgufFileHeader" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptBunLockEntry" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptPnpmLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/VcpkgManifest" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PnpmLockResolution": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "integrity" + ], + "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmFileEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the file download URL" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest is the hash digest of the file hosted at the URL" + } + }, + "type": "object", + "required": [ + "url", + "digest" + ] + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests (for the base package without extras)" + }, + "marker": { + "type": "string", + "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment" + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications for the base package (without extras)" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPdmLockExtraVariant" + }, + "type": "array", + "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)" + } + }, + "type": "object", + "required": [ + "summary", + "files" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPdmLockExtraVariant": { + "properties": { + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependencies specific to this extras variant" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files specific to this variant (only populated if different from base)" + }, + "marker": { + "type": "string", + "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")" + } + }, + "type": "object", + "required": [ + "extras" + ], + "description": "PythonPdmLockExtraVariant represents a specific extras combination variant within a PDM lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string", + "description": "Parent is the ID of the parent artifact in this relationship." + }, + "child": { + "type": "string", + "description": "Child is the ID of the child artifact in this relationship." + }, + "type": { + "type": "string", + "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")." + }, + "metadata": { + "description": "Metadata contains additional relationship-specific metadata." + } + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ], + "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package." + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "Schema": { + "properties": { + "version": { + "type": "string", + "description": "Version is the JSON schema version for this document format." + }, + "url": { + "type": "string", + "description": "URL is the URL to the JSON schema definition document." + } + }, + "type": "object", + "required": [ + "version", + "url" + ], + "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format." + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for the analyzed source artifact." + }, + "name": { + "type": "string", + "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)." + }, + "version": { + "type": "string", + "description": "Version is the version of the analyzed artifact (e.g., image tag)." + }, + "supplier": { + "type": "string", + "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance." + }, + "type": { + "type": "string", + "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")." + }, + "metadata": { + "description": "Metadata contains additional source-specific metadata." + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "VcpkgManifest": { + "properties": { + "description": { + "items": { + "type": "string" + }, + "type": "array" + }, + "documentation": { + "type": "string" + }, + "full-version": { + "type": "string" + }, + "version": { + "type": "string" + }, + "port-version": { + "type": "integer" + }, + "maintainers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "supports": { + "type": "string" + }, + "registry": { + "$ref": "#/$defs/VcpkgRegistryEntry", + "description": "to show where it came from" + }, + "triplet": { + "type": "string", + "description": "found by looking at build folder to find target. ex. \"x64-linux\"" + } + }, + "type": "object", + "required": [ + "full-version", + "version", + "port-version", + "name" + ], + "description": "used for metadata." + }, + "VcpkgRegistryEntry": { + "properties": { + "baseline": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "packages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "path": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "repository": { + "type": "string" + } + }, + "type": "object", + "required": [ + "kind" + ], + "description": "Matches definition of Vcpkg \"Registry\"." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 71e1dd2ff..1fc811850 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.1.7/document", + "$id": "anchore.io/schema/syft/json/16.1.8/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -2927,6 +2927,9 @@ { "$ref": "#/$defs/TerraformLockProviderEntry" }, + { + "$ref": "#/$defs/VcpkgManifest" + }, { "$ref": "#/$defs/WordpressPluginEntry" } @@ -4385,6 +4388,86 @@ ], "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." }, + "VcpkgManifest": { + "properties": { + "description": { + "items": { + "type": "string" + }, + "type": "array" + }, + "documentation": { + "type": "string" + }, + "full-version": { + "type": "string" + }, + "version": { + "type": "string" + }, + "port-version": { + "type": "integer" + }, + "maintainers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "supports": { + "type": "string" + }, + "registry": { + "$ref": "#/$defs/VcpkgRegistryEntry", + "description": "to show where it came from" + }, + "triplet": { + "type": "string", + "description": "found by looking at build folder to find target. ex. \"x64-linux\"" + } + }, + "type": "object", + "required": [ + "full-version", + "version", + "port-version", + "name" + ], + "description": "used for metadata." + }, + "VcpkgRegistryEntry": { + "properties": { + "baseline": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "packages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "path": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "repository": { + "type": "string" + } + }, + "type": "object", + "required": [ + "kind" + ], + "description": "Matches definition of Vcpkg \"Registry\"." + }, "WordpressPluginEntry": { "properties": { "pluginInstallDirectory": { diff --git a/syft/cataloging/pkgcataloging/config.go b/syft/cataloging/pkgcataloging/config.go index e5bac0cb8..4a1e2cff6 100644 --- a/syft/cataloging/pkgcataloging/config.go +++ b/syft/cataloging/pkgcataloging/config.go @@ -2,6 +2,7 @@ package pkgcataloging import ( "github.com/anchore/syft/syft/pkg/cataloger/binary" + "github.com/anchore/syft/syft/pkg/cataloger/cpp" "github.com/anchore/syft/syft/pkg/cataloger/dotnet" "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/java" @@ -13,6 +14,7 @@ import ( type Config struct { Binary binary.ClassifierCatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"` + Cpp cpp.CatalogerConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"` Dotnet dotnet.CatalogerConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"` JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"` @@ -25,6 +27,7 @@ type Config struct { func DefaultConfig() Config { return Config{ Binary: binary.DefaultClassifierCatalogerConfig(), + Cpp: cpp.DefaultCatalogerConfig(), Dotnet: dotnet.DefaultCatalogerConfig(), Golang: golang.DefaultCatalogerConfig(), JavaArchive: java.DefaultArchiveCatalogerConfig(), @@ -44,6 +47,11 @@ func (c Config) WithBinaryConfig(cfg binary.ClassifierCatalogerConfig) Config { return c } +func (c Config) WithCppConfig(cfg cpp.CatalogerConfig) Config { + c.Cpp = cfg + return c +} + func (c Config) WithDotnetConfig(cfg dotnet.CatalogerConfig) Config { c.Dotnet = cfg return c diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier.go b/syft/format/internal/spdxutil/helpers/originator_supplier.go index e5d4c183c..8452d951a 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier.go @@ -138,6 +138,11 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f case pkg.SwiplPackEntry: author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail) + + case pkg.VcpkgManifest: + if len(metadata.Maintainers) > 0 { + author = metadata.Maintainers[0] + } } if typ == "" && author != "" { diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index d18075450..dec9d06f0 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -60,6 +60,7 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.GGUFFileHeader{}, pkg.DenoLockEntry{}, pkg.DenoRemoteLockEntry{}, + pkg.VcpkgManifest{}, ) tests := []struct { name string diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go index 267703ab0..5682e22da 100644 --- a/syft/format/internal/spdxutil/helpers/source_info.go +++ b/syft/format/internal/spdxutil/helpers/source_info.go @@ -86,6 +86,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from Terraform dependency lock file" case pkg.ModelPkg: answer = "acquired package info from AI artifact (e.g. GGUF File)" + case pkg.VcpkgPkg: + answer = "acquired package info from vcpkg manifest file" default: answer = "acquired package info from the following paths" } diff --git a/syft/format/internal/spdxutil/helpers/source_info_test.go b/syft/format/internal/spdxutil/helpers/source_info_test.go index e16b6f974..8757258ff 100644 --- a/syft/format/internal/spdxutil/helpers/source_info_test.go +++ b/syft/format/internal/spdxutil/helpers/source_info_test.go @@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) { "from conda metadata", }, }, + { + input: pkg.Package{ + Type: pkg.VcpkgPkg, + }, + expected: []string{ + "from vcpkg manifest", + }, + }, { input: pkg.Package{ Type: pkg.PortagePkg, diff --git a/syft/pkg/cataloger/cpp/capabilities.yaml b/syft/pkg/cataloger/cpp/capabilities.yaml index 2f27f4d7f..802646b75 100644 --- a/syft/pkg/cataloger/cpp/capabilities.yaml +++ b/syft/pkg/cataloger/cpp/capabilities.yaml @@ -1,5 +1,11 @@ # Cataloger capabilities. See ../README.md for documentation. +configs: # AUTO-GENERATED - config structs and their fields + cpp.CatalogerConfig: + fields: + - key: VcpkgAllowGitClone + description: VcpkgAllowGitClone enables cloning remote git registries to resolve vcpkg manifest dependencies (requires network access). + app_key: cpp.vcpkg-allow-git-clone catalogers: - ecosystem: c++ # MANUAL name: conan-cataloger # AUTO-GENERATED @@ -126,3 +132,49 @@ catalogers: default: false - name: package_manager.package_integrity_hash default: false + - ecosystem: c++ # MANUAL + name: vcpkg-manifest-cataloger # AUTO-GENERATED + type: generic # AUTO-GENERATED + source: # AUTO-GENERATED + file: syft/pkg/cataloger/cpp/cataloger.go + function: NewVcpkgManifestCataloger + config: cpp.CatalogerConfig # AUTO-GENERATED + selectors: # AUTO-GENERATED + - cpp + - declared + - directory + - language + - package + - vcpkg + parsers: # AUTO-GENERATED structure + - function: parseVcpkgManifest + detector: # AUTO-GENERATED + method: glob # AUTO-GENERATED + criteria: # AUTO-GENERATED + - '**/vcpkg.json' + metadata_types: # AUTO-GENERATED + - pkg.VcpkgManifest + package_types: # AUTO-GENERATED + - vcpkg + purl_types: # AUTO-GENERATED + - vcpkg + json_schema_types: # AUTO-GENERATED + - VcpkgManifest + capabilities: # MANUAL - preserved across regeneration + - name: license + default: true + - name: dependency.depth + default: + - direct + - indirect + - name: dependency.edges + default: complete + - name: dependency.kinds + default: + - runtime + - name: package_manager.files.listing + default: false + - name: package_manager.files.digests + default: false + - name: package_manager.package_integrity_hash + default: false diff --git a/syft/pkg/cataloger/cpp/cataloger.go b/syft/pkg/cataloger/cpp/cataloger.go index aa031204e..c4d2565d0 100644 --- a/syft/pkg/cataloger/cpp/cataloger.go +++ b/syft/pkg/cataloger/cpp/cataloger.go @@ -20,3 +20,35 @@ func NewConanInfoCataloger() pkg.Cataloger { return generic.NewCataloger("conan-info-cataloger"). WithParserByGlobs(parseConaninfo, "**/conaninfo.txt") } + +// vcpkg (the Microsoft C/C++ package manager) has two modes +// (https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode): +// - classic mode: `vcpkg install ` populates a central tree at $VCPKG_ROOT/installed/. +// - manifest mode: a vcpkg.json declares dependencies; after a build the resolved tree appears +// under vcpkg_installed/ (a vcpkg/status DB + per-triplet dirs). +// +// what is on disk depends on where in the lifecycle the scan happens: +// - source checkout (the dir-scan case): vcpkg.json (+ vcpkg-configuration.json, overlay ports) +// are present. vcpkg_installed/ is a build artifact and is gitignored, so it is NOT present; +// exact transitive versions live in the registry, not the manifest. +// - built artifact: vcpkg_installed/ holds the actually-installed truth. +// +// NewVcpkgManifestCataloger covers ONLY the manifest (dir/source) case: it reads vcpkg.json and its +// declared dependencies and resolves each dependency's manifest from the vcpkg registry. resolving the +// registry needs a local registry clone or a network clone, which is opt-in via +// CatalogerConfig.VcpkgAllowGitClone (wired to --enrich). +// +// it deliberately does NOT cover installed state: vcpkg_installed/ (vcpkg/status, per-package +// vcpkg.spdx.json, copyright, ABI info), the build triplet (a build-time choice recorded only under +// vcpkg_installed/), and classic-mode central installs. +// +// why there is no installed-state (vcpkg/status) cataloger yet: vcpkg_installed/ tends to be a gitignored +// build artifact, so whether it survives into a scannable target is pattern-dependent. it is dropped by slim +// multi-stage runtime images (the documented best practice copies only the app binary / binary cache +// between stages) and by dev/base images (which bootstrap the tool, not packages); no canonical public +// image ships a populated vcpkg/status by default. it is retained mainly in single-stage builder / CI / +// "fat" app images. so a status-based cataloger has real but not universal payoff and is deferred until +// it is worth the maintenance. +func NewVcpkgManifestCataloger(opts CatalogerConfig) pkg.Cataloger { + return generic.NewCataloger("vcpkg-manifest-cataloger").WithParserByGlobs(newVcpkgCataloger(opts.VcpkgAllowGitClone).parseVcpkgManifest, "**/vcpkg.json") +} diff --git a/syft/pkg/cataloger/cpp/cataloger_test.go b/syft/pkg/cataloger/cpp/cataloger_test.go index 99b4543c9..cd34d7c4c 100644 --- a/syft/pkg/cataloger/cpp/cataloger_test.go +++ b/syft/pkg/cataloger/cpp/cataloger_test.go @@ -6,7 +6,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) -func TestCataloger_Globs(t *testing.T) { +func TestCatalogerConan_Globs(t *testing.T) { tests := []struct { name string fixture string @@ -32,7 +32,7 @@ func TestCataloger_Globs(t *testing.T) { } } -func TestCatalogerInfo_Globs(t *testing.T) { +func TestCatalogerConanInfo_Globs(t *testing.T) { tests := []struct { name string fixture string diff --git a/syft/pkg/cataloger/cpp/config.go b/syft/pkg/cataloger/cpp/config.go new file mode 100644 index 000000000..6568d9a25 --- /dev/null +++ b/syft/pkg/cataloger/cpp/config.go @@ -0,0 +1,19 @@ +package cpp + +type CatalogerConfig struct { + // VcpkgAllowGitClone enables cloning remote git registries to resolve vcpkg manifest dependencies (requires network access). + // app-config: cpp.vcpkg-allow-git-clone + VcpkgAllowGitClone bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"` +} + +func DefaultCatalogerConfig() CatalogerConfig { + return CatalogerConfig{ + // syft defaults to not sending requests over the network. You must opt in + VcpkgAllowGitClone: false, + } +} + +func (c CatalogerConfig) WithVcpkgAllowGitClone(input bool) CatalogerConfig { + c.VcpkgAllowGitClone = input + return c +} diff --git a/syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go b/syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go new file mode 100644 index 000000000..302b49850 --- /dev/null +++ b/syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go @@ -0,0 +1,697 @@ +package vcpkg + +import ( + "context" + "encoding/json" + "fmt" + "os" + "slices" + "strconv" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage" + "github.com/go-git/go-git/v5/storage/memory" + + "github.com/anchore/syft/internal/cache" + "github.com/anchore/syft/syft/pkg" +) + +// this is the default registry for vcpkg. it is the default "builtin" registry if a builtin one isn't specified +var defaultRegistry = pkg.VcpkgRegistryEntry{ + Baseline: "master", + Kind: pkg.Git, + Repository: "https://github.com/microsoft/vcpkg", +} + +// represents contents of "vcpkg.json" file. (a.k.a the manifest file) +type Vcpkg struct { + BuiltinBaseline string `json:"builtin-baseline,omitempty"` + DefaultFeatures []any `json:"default-features,omitempty"` + // string or []VcpkgDependency + Dependencies []any `json:"dependencies,omitempty"` + // string or []string + Description any `json:"description,omitempty"` + Documentation string `json:"documentation,omitempty"` + Features map[string]vcpkgFeatureEntry `json:"features,omitempty"` + Homepage string `json:"homepage,omitempty"` + // In SPDX license expression format. see https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json + License string `json:"license,omitempty"` + Maintainers []string `json:"maintainers,omitempty"` + Name string `json:"name,omitempty"` + // Only overrides defined by the top-level project are used + Overrides []vcpkgOverrideEntry `json:"overrides,omitempty"` + PortVersion float64 `json:"port-version,omitempty"` + Supports string `json:"supports,omitempty"` + // at most one of these version fields will be present and represent different versioning strategies + // see https://learn.microsoft.com/en-us/vcpkg/users/versioning#version-schemes for more details + Version string `json:"version,omitempty"` + VersionSemver string `json:"version-semver,omitempty"` + VersionDate string `json:"version-date,omitempty"` + VersionString string `json:"version-string,omitempty"` +} + +// Confusingly not the same as Feature Object +// see https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json#feature vs https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json#feature-object +type vcpkgFeatureEntry struct { + Description string `json:"description"` + // string or []VcpkgDependency + Dependencies []any `json:"dependencies,omitempty"` + // "Platform expression" + Supports string `json:"supports,omitempty"` + // SPDX license expression + License string `json:"license,omitempty"` +} + +type vcpkgOverrideEntry struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` + VersionSemver string `json:"version-semver,omitempty"` + VersionDate string `json:"version-date,omitempty"` + VersionString string `json:"version-string,omitempty"` + PortVersion float64 `json:"port-version,omitempty"` +} + +// represents contents of "vcpkg-configuration.json" file +type Config struct { + DefaultRegistry *pkg.VcpkgRegistryEntry `json:"default-registry"` + OverlayPorts []string `json:"overlay-ports,omitempty"` + OverlayTriplets []string `json:"overlay-triplets,omitempty"` + Registries []pkg.VcpkgRegistryEntry `json:"registries,omitempty"` +} + +// used to get specific dependency from git history +type vcpkgGitVersionObjectEntry struct { + // Sha1 value used to retrieve specific git tree object from Github. https://docs.github.com/en/rest/git/trees?apiVersion=2022-11-28 + GitTree string `json:"git-tree"` + Version string `json:"version,omitempty"` + VersionSemver string `json:"version-semver,omitempty"` + VersionDate string `json:"version-date,omitempty"` + VersionString string `json:"version-string,omitempty"` + PortVersion float64 `json:"port-version"` +} + +// represents versions file ".json" found in versions folder +type vcpkgGitVersions struct { + Versions []vcpkgGitVersionObjectEntry `json:"versions"` +} + +// Filesystem VersionObject +type vcpkgFsVersionObjectEntry struct { + Path string `json:"path"` + Version string `json:"version,omitempty"` + VersionSemver string `json:"version-semver,omitempty"` + VersionDate string `json:"version-date,omitempty"` + VersionString string `json:"version-string,omitempty"` + PortVersion float64 `json:"port-version"` +} + +// represents filesystem versions file ".json" found in versions folder +type vcpkgFsVersions struct { + Versions []vcpkgFsVersionObjectEntry `json:"versions"` +} + +// helpful to define relationships between Vcpkgs +// ResolvedManifest is a source manifest (vcpkg.json) together with the registry it was resolved from. +// it carries the raw Vcpkg so the cataloger can build the SBOM metadata and set package-level fields +// (e.g. license) on pkg.Package rather than on the metadata. +type ResolvedManifest struct { + Vcpkg *Vcpkg + Registry *pkg.VcpkgRegistryEntry +} + +type ManifestNode struct { + Parent *ResolvedManifest + Child *ResolvedManifest +} + +type vcpkgBaselineVersionObjectEntry struct { + Baseline string `json:"baseline"` + PortVersion float64 `json:"port-version"` +} + +func (v *Vcpkg) GetFullVersion() string { + popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) + if v.PortVersion != 0 { + return popVer + "#" + strconv.Itoa(int(v.PortVersion)) + } + return popVer +} + +func (v *vcpkgGitVersionObjectEntry) GetFullVersion() string { + popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) + if v.PortVersion != 0 { + return popVer + "#" + strconv.Itoa(int(v.PortVersion)) + } + return popVer +} + +func (v *vcpkgFsVersionObjectEntry) GetFullVersion() string { + popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) + if v.PortVersion != 0 { + return popVer + "#" + strconv.Itoa(int(v.PortVersion)) + } + return popVer +} + +func (v *vcpkgOverrideEntry) GetFullVersion() string { + popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) + if v.PortVersion != 0 { + return popVer + "#" + strconv.Itoa(int(v.PortVersion)) + } + return popVer +} + +func (v *vcpkgGitVersionObjectEntry) GetPopulatedVersion() string { + return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) +} + +func (v *vcpkgFsVersionObjectEntry) GetPopulatedVersion() string { + return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) +} + +func (v *Vcpkg) GetPopulatedVersion() string { + return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString) +} + +func getPopulatedVersion(version, versionSemver, versionDate, versionString string) string { + switch { + case version != "": + return version + case versionSemver != "": + return versionSemver + case versionDate != "": + return versionDate + case versionString != "": + return versionString + default: + return "" + } +} + +const vcpkgRepo string = "https://github.com/microsoft/vcpkg" + +type ID struct { + location string + head string + name string + version string +} + +// Resolver is a short-lived utility to resolve vcpkg manifests from multiple sources, including: +// the filesystem, local maven cache directories, remote maven repositories, and the syft cache +type Resolver struct { + ctx context.Context + gitRepos map[string]storage.Storer + allowGitClone bool + cfg *Config + resolved map[ID]*ResolvedManifest +} + +// NewResolver constructs a new Resolver with the given vcpkg configuration. +func NewResolver(ctx context.Context, cfg *Config, allowGitClone bool) *Resolver { + return &Resolver{ + ctx: ctx, + gitRepos: map[string]storage.Storer{}, + allowGitClone: allowGitClone, + cfg: cfg, + resolved: map[ID]*ResolvedManifest{}, + } +} + +// Get all of the manifest/vcpkg.json files for a vcpkg dependency +func (r *Resolver) FindManifests(dependency any, df bool, builtinBaseline string, overrides []vcpkgOverrideEntry, parent *ResolvedManifest) ([]ManifestNode, error) { + var name string + var fullVersion string + defaultFeatures := df + var features []any + // can be either string or VcpkgDependency + switch d := dependency.(type) { + case string: + name = d + case map[string]any: + if n, ok := d["name"].(string); ok { + name = n + } + if v, ok := d["version>="].(string); ok { + fullVersion = v + } + if v, ok := d["default-features"].(bool); ok { + defaultFeatures = defaultFeatures && v + } + if v, ok := d["features"].([]any); ok { + features = v + } + } + // for when top-level manifest has this dependency version overridden + if over, ok := depVerOverriden(name, overrides); ok { + fullVersion = over.Version + } + reg := r.depRegistry(name, builtinBaseline) + vcpkg, err := r.findManifestFromReg(reg, name, fullVersion) + if err != nil { + return nil, err + } + child := &ResolvedManifest{Vcpkg: vcpkg, Registry: reg} + // need to add dependency to map, even if it doesn't find its manifest, to avoid infinite loops caused by circular depenencies + switch reg.Kind { + case pkg.Git: + r.resolved[ID{reg.Repository, reg.Baseline, name, fullVersion}] = child + case pkg.Builtin: + vcpkgRoot := os.Getenv("VCPKG_ROOT") + r.resolved[ID{vcpkgRoot, reg.Baseline, name, fullVersion}] = child + case pkg.FileSystem: + r.resolved[ID{reg.Path, reg.Baseline, name, fullVersion}] = child + } + vcpkg.appendFtrDepsToDeps(defaultFeatures, features) + + manNodes := []ManifestNode{} + manNodes = append(manNodes, ManifestNode{ + Parent: parent, + Child: child, + }) + if len(vcpkg.Dependencies) != 0 { + for _, dep := range vcpkg.Dependencies { + resolvedManifest, ok := r.depResolved(dep, builtinBaseline) + if ok { + // this is to catch duplicates + manNodes = append(manNodes, ManifestNode{ + // child is parent in this case + Parent: child, + Child: resolvedManifest, + }) + } else { + childManNodes, err := r.FindManifests(dep, df, builtinBaseline, overrides, child) + if err != nil { + return nil, fmt.Errorf("could not find vcpkg.json file for dependency. %w", err) + } + manNodes = append(manNodes, childManNodes...) + } + } + } + return manNodes, nil +} + +func (v *Vcpkg) appendFtrDepsToDeps(df bool, features []any) { + // collect explicitly requested feature names (a feature is either a string or a feature object) + requested := map[string]bool{} + for _, feature := range features { + switch fo := feature.(type) { + case string: + requested[fo] = true + case map[string]any: + // json decodes a feature object into map[string]any, not a struct + if name, ok := fo["name"].(string); ok { + requested[name] = true + } + } + } + // add each feature's deps at most once: when requested, or when it's a default feature and defaults are enabled. + // this must be independent of the requested loop so default features apply even when none are requested. + for name, f := range v.Features { + if requested[name] || (df && isDefaultFeature(name, v.DefaultFeatures)) { + v.Dependencies = append(v.Dependencies, f.Dependencies...) + } + } +} + +func depVerOverriden(name string, overrides []vcpkgOverrideEntry) (*vcpkgOverrideEntry, bool) { + for _, over := range overrides { + if over.Name == name { + return &over, true + } + } + return nil, false +} + +func (r *Resolver) resolveRepo(repoStr string) (*git.Repository, error) { + if r.gitRepos[repoStr] != nil { + return git.Open(r.gitRepos[repoStr], nil) + } else if r.allowGitClone { + repo, err := git.CloneContext(r.ctx, memory.NewStorage(), nil, &git.CloneOptions{ + URL: repoStr, + }) + if err != nil { + return nil, err + } + r.gitRepos[repoStr] = repo.Storer + return repo, nil + } + return nil, fmt.Errorf("could not resolve %s. enable vcpkg-allow-git-clone flag to allow cloning of remote git repos", repoStr) +} + +func (r *Resolver) getRepo(repoStr string, path *string) (*git.Repository, error) { + vcpkgCachePath := getVcpkgGitCachePath() + // best to use vcpkg cache. Only able to locate if it's in the same directory as syft cache + if r.gitRepos[repoStr] == nil && vcpkgCachePath != "" { + repo, err := git.PlainOpen(vcpkgCachePath) + if err == nil { + r.gitRepos[repoStr] = repo.Storer + return repo, err + } + } + if r.gitRepos[repoStr] == nil && path != nil { + repo, err := git.PlainOpen(*path) + if err == nil { + r.gitRepos[repoStr] = repo.Storer + return repo, err + } + } + return r.resolveRepo(repoStr) +} + +// get path of vcpkg cache +func getVcpkgGitCachePath() string { + roots := cache.GetManager().RootDirs() + if len(roots) == 0 { + return "" + } + // vcpkg keeps its git registry cache as a sibling of the syft cache dir + return roots[0] + "/../vcpkg/registries/git" +} + +// determines which registry to use by the name of the dependency +func (r *Resolver) depRegistry(name string, builtinBaseline string) *pkg.VcpkgRegistryEntry { + var reg *pkg.VcpkgRegistryEntry + if r.cfg != nil { + for _, res := range r.cfg.Registries { + if slices.Contains(res.Packages, name) { + reg = &res + } + } + if r.cfg.DefaultRegistry != nil && reg == nil { + reg = r.cfg.DefaultRegistry + } + } + if reg == nil { + // copy the package-global so concurrent catalogers don't clobber its baseline + def := defaultRegistry + def.Baseline = builtinBaseline + reg = &def + } else if reg.Kind == pkg.Builtin { + reg.Baseline = builtinBaseline + } + return reg +} + +// checks if dependency has been retrieved already this run. Without this check, there were infinite loops from circular dependencies +func (r *Resolver) depResolved(dep any, builtinBaseline string) (*ResolvedManifest, bool) { + var name string + var version string + switch d := dep.(type) { + case string: + name = d + case map[string]any: + if n, ok := d["name"].(string); ok { + name = n + } + if v, ok := d["version>="].(string); ok { + version = v + } + } + reg := r.depRegistry(name, builtinBaseline) + var location string + switch { + case reg.Repository != "": + location = reg.Repository + case reg.Path != "": + location = reg.Path + default: + location = vcpkgRepo + } + resolved, ok := r.resolved[ID{location, reg.Baseline, name, version}] + return resolved, ok +} + +func isDefaultFeature(name string, defaultFeatures []any) bool { + for _, df := range defaultFeatures { + switch d := df.(type) { + case string: + if name == d { + return true + } + case map[string]any: + if n, ok := d["name"].(string); ok && name == n { + return true + } + } + } + return false +} + +func (r *Resolver) findManifestFromReg(reg *pkg.VcpkgRegistryEntry, name, fullVersion string) (*Vcpkg, error) { + if reg == nil { + return nil, fmt.Errorf("no vcpkg registry found which is required") + } + switch reg.Kind { + case pkg.Git: + var vcpkg *Vcpkg + var err error + if reg.Repository == "" { + return nil, fmt.Errorf("no repo found for vcpkg git registry") + } + if strings.TrimSuffix(reg.Repository, ".git") == vcpkgRepo { + path := os.Getenv("VCPKG_ROOT") + gitRepo, err := r.getRepo(vcpkgRepo, &path) + if err != nil { + return nil, err + } + vcpkg, err = r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion) + if err != nil { + return nil, err + } + } else { + gitRepo, err := r.getRepo(reg.Repository, nil) + if err != nil { + return nil, err + } + vcpkg, err = r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion) + if err != nil { + return nil, err + } + } + return vcpkg, err + case pkg.Builtin: + path := os.Getenv("VCPKG_ROOT") + gitRepo, err := r.getRepo(vcpkgRepo, &path) + if err != nil { + return nil, err + } + vcpkg, err := r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion) + if err != nil { + return nil, err + } + return vcpkg, err + case pkg.FileSystem: + if reg.Path == "" { + return nil, fmt.Errorf("no path found for vcpkg filesystem registry") + } + baseline := reg.Baseline + if baseline == "" { + baseline = "default" + } + vcpkg, err := r.getManifestFromFilesystem(reg.Path, baseline, name, fullVersion) + if err != nil { + return nil, err + } + return vcpkg, err + default: + return nil, fmt.Errorf("vcpkg registry has no kind which is required") + } +} + +// locates and gets the manifest file via the go-git +func (r *Resolver) getManifestFromGitRepo(repo *git.Repository, head, name, fullVersion string) (*Vcpkg, error) { + headObj, err := repo.CommitObject(plumbing.NewHash(head)) + if err != nil { + return nil, err + } + tree, err := headObj.Tree() + if err != nil { + return nil, err + } + var resultMan Vcpkg + if fullVersion != "" { + verPath := "versions/" + name[0:1] + "-/" + name + ".json" + verFile, err := findFileInTree(verPath, tree) + if err != nil { + return nil, fmt.Errorf("failed to get versions file from vcpkg git tree. %w", err) + } + content, err := verFile.Contents() + if err != nil { + return nil, fmt.Errorf("failed to get contents of versions file from vcpkg git tree. %w", err) + } + var versions vcpkgGitVersions + err = json.Unmarshal([]byte(content), &versions) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal versions file from vcpkg git tree. %w", err) + } + // get tree object sha for the port version + var gitTreeHash string + for _, v := range versions.Versions { + if fullVersion == v.GetFullVersion() { + gitTreeHash = v.GitTree + break + } + } + if gitTreeHash == "" { + return nil, fmt.Errorf("version %q not found in versions file for port %q", fullVersion, name) + } + verTree, err := repo.TreeObject(plumbing.NewHash(gitTreeHash)) + if err != nil { + return nil, fmt.Errorf("failed to get tree from hash %v. %w", gitTreeHash, err) + } + manFile, err := verTree.File("vcpkg.json") + if err != nil { + return nil, fmt.Errorf("failed to get vcpkg.json file from git tree. %w", err) + } + manContents, err := manFile.Contents() + if err != nil { + return nil, fmt.Errorf("failed to get contents of vcpkg.json file from git tree. %w", err) + } + err = json.Unmarshal([]byte(manContents), &resultMan) + if err != nil { + return nil, err + } + } else { + portPath := "ports/" + name + "/vcpkg.json" + manFile, err := findFileInTree(portPath, tree) + if err != nil { + return nil, fmt.Errorf("failed to get vcpkg.json file from ports directory in git tree. %w", err) + } + manContents, err := manFile.Contents() + if err != nil { + return nil, fmt.Errorf("failed to get contents of vcpkg.json file from ports directory in git tree. %w", err) + } + err = json.Unmarshal([]byte(manContents), &resultMan) + if err != nil { + return nil, err + } + } + return &resultMan, err +} + +func findFileInTree(path string, tree *object.Tree) (*object.File, error) { + verFile, err := tree.File(path) + return verFile, err +} + +// locates and gets the manifest file via the filesystem path +func (r *Resolver) getManifestFromFilesystem(path, baseline, name, fullVersion string) (*Vcpkg, error) { + if path == "" { + return &Vcpkg{}, fmt.Errorf("no/empty path specified for vcpkg filesystem registry") + } + var finalVer string + if fullVersion == "" { + baselineBytes, err := os.ReadFile(path + "/versions/baseline.json") + if err != nil { + return &Vcpkg{}, err + } + var baselineGen map[string]any + err = json.Unmarshal(baselineBytes, &baselineGen) + if err != nil { + return &Vcpkg{}, err + } + var baselineMatch map[string]any + for k, v := range baselineGen { + if k == baseline { + baselineMatch, _ = v.(map[string]any) + break + } + } + var baselineVer vcpkgBaselineVersionObjectEntry + for k, v := range baselineMatch { + if k == name { + foundBaseline, ok := v.(map[string]any) + if !ok { + continue + } + bl, _ := foundBaseline["baseline"].(string) + // default for go json package when unmarshalling number is float64 + pv, _ := foundBaseline["port-version"].(float64) + baselineVer = vcpkgBaselineVersionObjectEntry{ + Baseline: bl, + PortVersion: pv, + } + } + } + if baselineVer.Baseline == "" { + return nil, fmt.Errorf("could not find a baseline version for dependency with name %v", name) + } + if baselineVer.PortVersion > 0 { + finalVer = baselineVer.Baseline + "#" + strconv.Itoa(int(baselineVer.PortVersion)) + } else { + finalVer = baselineVer.Baseline + } + } else { + finalVer = fullVersion + } + return getFsManifest(path, name, finalVer) +} + +// retrieves manifest file from the filesystem +func getFsManifest(path, name, ver string) (*Vcpkg, error) { + versionBytes, err := os.ReadFile(path + "/versions/" + name[0:1] + "-/" + name + ".json") + if err != nil { + return nil, err + } + var verFileCont vcpkgFsVersions + err = json.Unmarshal(versionBytes, &verFileCont) + if err != nil { + return nil, err + } + for _, v := range verFileCont.Versions { + if v.GetFullVersion() == ver { + // the $ character can be used to reference the root of the registry + manifestPath := strings.ReplaceAll(v.Path, "$", path) + manBytes, err := os.ReadFile(manifestPath + "/vcpkg.json") + if err != nil { + return nil, err + } + var vcpkgManRes Vcpkg + err = json.Unmarshal(manBytes, &vcpkgManRes) + if err != nil { + return nil, err + } + return &vcpkgManRes, nil + } + } + return nil, fmt.Errorf("failed to find vcpkg.json file for dependency name: %v", name) +} + +func (v *Vcpkg) BuildManifest(reg *pkg.VcpkgRegistryEntry, triplet string) *pkg.VcpkgManifest { + var desc []string + switch d := v.Description.(type) { + case string: + desc = append(desc, d) + case []any: + // json decodes a description array into []any, not []string + for _, item := range d { + if s, ok := item.(string); ok { + desc = append(desc, s) + } + } + } + // give each manifest its own registry copy: the resolver hands out a shared *VcpkgRegistryEntry + // (e.g. cfg.DefaultRegistry) to every package, and a shared pointer means one package's metadata + // can alias another's + var regCopy *pkg.VcpkgRegistryEntry + if reg != nil { + r := *reg + regCopy = &r + } + return &pkg.VcpkgManifest{ + Description: desc, + Documentation: v.Documentation, + FullVersion: v.GetFullVersion(), + Version: v.GetPopulatedVersion(), + PortVersion: int(v.PortVersion), + Maintainers: v.Maintainers, + Name: v.Name, + Supports: v.Supports, + Registry: regCopy, + Triplet: triplet, + } +} diff --git a/syft/pkg/cataloger/cpp/package.go b/syft/pkg/cataloger/cpp/package.go index bc003ea7f..fbc5c2cad 100644 --- a/syft/pkg/cataloger/cpp/package.go +++ b/syft/pkg/cataloger/cpp/package.go @@ -1,11 +1,15 @@ package cpp import ( + "context" + "net/url" + "strconv" "strings" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/cpp/internal/vcpkg" ) type conanRef struct { @@ -92,7 +96,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p Name: ref.Name, Version: ref.Version, Locations: file.NewLocationSet(locations...), - PURL: packageURL(ref), + PURL: packageURLFromConanRef(ref), Language: pkg.CPP, Type: pkg.ConanPkg, Metadata: metadata, @@ -103,7 +107,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p return &p } -func packageURL(ref *conanRef) string { +func packageURLFromConanRef(ref *conanRef) string { qualifiers := packageurl.Qualifiers{} if ref.Channel != "" { qualifiers = append(qualifiers, packageurl.Qualifier{ @@ -120,3 +124,63 @@ func packageURL(ref *conanRef) string { "", ).ToString() } + +func newVcpkgPackage(ctx context.Context, rm *vcpkg.ResolvedManifest, l file.Location) pkg.Package { + // build the SBOM metadata from the source manifest; license is a package-level field (pkg.Package), + // not metadata, so it is read off the source manifest here rather than carried in pkg.VcpkgManifest + man := rm.Vcpkg.BuildManifest(rm.Registry, "") + p := pkg.Package{ + Name: man.Name, + Version: man.FullVersion, + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, rm.Vcpkg.License, l)), + Locations: file.NewLocationSet(l), + PURL: packageURLFromVcpkgManifest(man), + Language: pkg.CPP, + Type: pkg.VcpkgPkg, + Metadata: man, + } + + p.SetID() + return p +} + +func packageURLFromVcpkgManifest(v *pkg.VcpkgManifest) string { + qualifiers := packageurl.Qualifiers{} + // + if v.Triplet != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "triplet", + Value: v.Triplet, + }) + } + if v.PortVersion != 0 { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "port_revision", + Value: strconv.Itoa(v.PortVersion), + }) + } + if v.Registry != nil && v.Registry.Repository != "" { + unescRepoURL, err := url.PathUnescape(v.Registry.Repository) + if err == nil { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "repository_url", + Value: unescRepoURL, + }) + } + } + if v.Registry != nil && v.Registry.Baseline != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "repository_revision", + Value: v.Registry.Baseline, + }) + } + return packageurl.NewPackageURL( + // https://github.com/package-url/purl-spec/pull/245 + "vcpkg", + "", + v.Name, + v.Version, + qualifiers, + "", + ).ToString() +} diff --git a/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go new file mode 100644 index 000000000..cf11d15bb --- /dev/null +++ b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go @@ -0,0 +1,182 @@ +package cpp + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/cpp/internal/vcpkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +// vcpkg manifests are small JSON files; cap reads to guard against pathological inputs +const maxVcpkgManifestSize = 5 * 1024 * 1024 + +type vcpkgCataloger struct { + allowGitClone bool +} + +func newVcpkgCataloger(allowGitClone bool) *vcpkgCataloger { + return &vcpkgCataloger{ + allowGitClone: allowGitClone, + } +} + +// parser is for vcpkg in "Manifest" mode. This is opposed to "Classic" mode which or is more akin to a system package manager. (https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode) +func (v *vcpkgCataloger) parseVcpkgManifest(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + conf, err := findVcpkgConfig(resolver) + if err != nil { + return nil, nil, fmt.Errorf("something went wrong parsing vcpkg-configuration.json file: %w", err) + } + + var toplevelVcpkg vcpkg.Vcpkg + dec := json.NewDecoder(reader) + err = dec.Decode(&toplevelVcpkg) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse vcpkg.json file: %w", err) + } + + var vcpkgs []vcpkg.Vcpkg + vcpkgs = append(vcpkgs, toplevelVcpkg) + overlayVcpkgs, err := findOverlayManifests(resolver, conf.OverlayPorts) + if err != nil { + return nil, nil, fmt.Errorf("could not get overlay port manifests: %w", err) + } + vcpkgs = append(vcpkgs, overlayVcpkgs...) + var pkgs []pkg.Package + var relationships []artifact.Relationship + for _, parentVcpkg := range vcpkgs { + pv := parentVcpkg + // the top-level project manifest has no registry (it is the thing being scanned, not a resolved dep) + parentMan := &vcpkg.ResolvedManifest{Vcpkg: &pv, Registry: nil} + pPkg := newVcpkgPackage(ctx, parentMan, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + pkgs = append( + pkgs, + pPkg) + + r := vcpkg.NewResolver( + ctx, + conf, + v.allowGitClone, + ) + for _, dep := range parentVcpkg.Dependencies { + cMans, fetchErr := r.FindManifests(dep, true, toplevelVcpkg.BuiltinBaseline, toplevelVcpkg.Overrides, parentMan) + if fetchErr != nil { + // best-effort: a single unresolvable dependency shouldn't discard packages already found + log.Debugf("vcpkg: unable to resolve dependency in %q: %v", parentVcpkg.Name, fetchErr) + continue + } + pkgs, relationships = appendPkgsAndRelationships(ctx, cMans, overlayVcpkgs, reader, relationships, pkgs) + } + } + pkg.Sort(pkgs) + return pkgs, relationships, nil +} + +func appendPkgsAndRelationships(ctx context.Context, cMans []vcpkg.ManifestNode, overlayVcpkgs []vcpkg.Vcpkg, reader file.LocationReadCloser, relationships []artifact.Relationship, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) { + p := pkgs + r := relationships + for _, c := range cMans { + if c.Child != nil && !hasBeenOverlayed(c.Child.Vcpkg.Name, overlayVcpkgs) { + cPkg := newVcpkgPackage(ctx, c.Child, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + if c.Parent != nil { + pPkg := newVcpkgPackage(ctx, c.Parent, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + pPkg.FoundBy = "vcpkg-manifest-cataloger" + cPkg.FoundBy = "vcpkg-manifest-cataloger" + rship := artifact.Relationship{ + // From is the dependency, To is the dependent (syft convention) + From: cPkg, + To: pPkg, + Type: artifact.DependencyOfRelationship, + } + r = append( + r, + rship) + } + p = append( + p, + cPkg) + } + } + return p, r +} + +// These are to be used in place of dependencies with the same name +func findOverlayManifests(resolver file.Resolver, overlayPorts []string) ([]vcpkg.Vcpkg, error) { + var manifests []vcpkg.Vcpkg + for _, op := range overlayPorts { + // overlay port path is relative to location of vcpkg-configuration.json file + locs, err := resolver.FilesByGlob(op + "/**/vcpkg.json") + if err != nil { + return nil, err + } + for _, loc := range locs { + man, err := findManAtLoc(loc, resolver) + if err != nil { + return nil, err + } + manifests = append(manifests, *man) + } + } + return manifests, nil +} + +func findManAtLoc(loc file.Location, resolver file.Resolver) (*vcpkg.Vcpkg, error) { + manCont, err := resolver.FileContentsByLocation(loc) + if err != nil { + return nil, err + } + defer internal.CloseAndLogError(manCont, loc.RealPath) + manBytes, err := io.ReadAll(io.LimitReader(manCont, maxVcpkgManifestSize)) + if err != nil { + return nil, err + } + var man vcpkg.Vcpkg + err = json.Unmarshal(manBytes, &man) + if err != nil { + return nil, err + } + return &man, nil +} + +// check to see if a package is not going to get pulled in because it's apart of the overlay-ports in vcpkg-configuration.json file +func hasBeenOverlayed(pkgName string, overlayMans []vcpkg.Vcpkg) bool { + for _, om := range overlayMans { + if om.Name == pkgName { + return true + } + } + return false +} + +// needed to know what vcpkg registries to use for what packages when looking for manifest files +func findVcpkgConfig(resolver file.Resolver) (*vcpkg.Config, error) { + locs, err := resolver.FilesByGlob("**/vcpkg-configuration.json") + if err != nil { + return nil, err + } + if len(locs) != 0 { + cfgCont, err := resolver.FileContentsByLocation(locs[0]) + if err != nil { + return nil, err + } + defer internal.CloseAndLogError(cfgCont, locs[0].RealPath) + cfgBytes, err := io.ReadAll(io.LimitReader(cfgCont, maxVcpkgManifestSize)) + if err != nil { + return nil, err + } + var vcpkgConf vcpkg.Config + err = json.Unmarshal(cfgBytes, &vcpkgConf) + if err != nil { + return nil, err + } + return &vcpkgConf, err + } + return &vcpkg.Config{}, nil +} diff --git a/syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go new file mode 100644 index 000000000..cd86a4b45 --- /dev/null +++ b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go @@ -0,0 +1,244 @@ +package cpp + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/internal/cache" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func TestParseVcpkgManifest(t *testing.T) { + + fixture := "testdata/vcpkg/helloworld" + // the resolver reads the vcpkg registry from a local git clone rather than cloning over the network + // at test time. the clone is materialized just-in-time on first run (network required once) into a + // gitignored cache dir, then reused offline on later runs. + useLocalVcpkgRegistryCache(t) + fileLocs := []file.Location{file.NewLocation("vcpkg.json")} + fixtureLocationSet := file.NewLocationSet(fileLocs...) + ctx := pkgtest.Context(t) + + fmtPkg := pkg.Package{ + Name: "fmt", + Version: "11.0.2#1", + PURL: "pkg:vcpkg/fmt@11.0.2?port_revision=1&repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture", + FoundBy: "vcpkg-manifest-cataloger", + Locations: fixtureLocationSet, + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)), + Language: pkg.CPP, + Type: pkg.VcpkgPkg, + Metadata: &pkg.VcpkgManifest{ + Description: []string{"{fmt} is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams."}, + FullVersion: "11.0.2#1", + Version: "11.0.2", + PortVersion: 1, + Name: "fmt", + Registry: &pkg.VcpkgRegistryEntry{ + Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3", + Kind: pkg.Git, + Repository: "https://github.com/anchore/vcpkg-test-fixture", + }, + }, + } + helloPkg := pkg.Package{ + Name: "hello", + Version: "0.1.0", + PURL: "pkg:vcpkg/hello@0.1.0", + FoundBy: "vcpkg-manifest-cataloger", + Locations: fixtureLocationSet, + Language: pkg.CPP, + Type: pkg.VcpkgPkg, + Metadata: &pkg.VcpkgManifest{ + FullVersion: "0.1.0", + Version: "0.1.0", + Name: "hello", + Registry: nil, + }, + } + vcpkgCmakeConfigPkg := pkg.Package{ + Name: "vcpkg-cmake-config", + Version: "2024-05-23", + PURL: "pkg:vcpkg/vcpkg-cmake-config@2024-05-23?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture", + FoundBy: "vcpkg-manifest-cataloger", + Locations: fixtureLocationSet, + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)), + Language: pkg.CPP, + Type: pkg.VcpkgPkg, + Metadata: &pkg.VcpkgManifest{ + Documentation: "https://learn.microsoft.com/vcpkg/maintainers/functions/vcpkg_cmake_config_fixup", + FullVersion: "2024-05-23", + Version: "2024-05-23", + Name: "vcpkg-cmake-config", + Registry: &pkg.VcpkgRegistryEntry{ + Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3", + Kind: pkg.Git, + Repository: "https://github.com/anchore/vcpkg-test-fixture", + }, + }, + } + vcpkgCmakePkg := pkg.Package{ + Name: "vcpkg-cmake", + Version: "2024-04-23", + PURL: "pkg:vcpkg/vcpkg-cmake@2024-04-23?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture", + FoundBy: "vcpkg-manifest-cataloger", + Locations: fixtureLocationSet, + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)), + Language: pkg.CPP, + Type: pkg.VcpkgPkg, + Metadata: &pkg.VcpkgManifest{ + Documentation: "https://learn.microsoft.com/vcpkg/maintainers/functions/vcpkg_cmake_configure", + FullVersion: "2024-04-23", + Version: "2024-04-23", + Name: "vcpkg-cmake", + Registry: &pkg.VcpkgRegistryEntry{ + Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3", + Kind: pkg.Git, + Repository: "https://github.com/anchore/vcpkg-test-fixture", + }, + }, + } + sampleLibPkg := pkg.Package{ + Name: "vcpkg-sample-library", + Version: "1.0.2", + PURL: "pkg:vcpkg/vcpkg-sample-library@1.0.2?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture", + FoundBy: "vcpkg-manifest-cataloger", + Locations: fixtureLocationSet, + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)), + Language: pkg.CPP, + Type: pkg.VcpkgPkg, + Metadata: &pkg.VcpkgManifest{ + Description: []string{"A sample C++ library designed to serve as a foundational example for a tutorial on packaging libraries with vcpkg."}, + FullVersion: "1.0.2", + Version: "1.0.2", + Name: "vcpkg-sample-library", + Registry: &pkg.VcpkgRegistryEntry{ + Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3", + Kind: pkg.Git, + Repository: "https://github.com/anchore/vcpkg-test-fixture", + }, + }, + } + + expectedPkgs := []pkg.Package{ + fmtPkg, + fmtPkg, + helloPkg, + vcpkgCmakeConfigPkg, + vcpkgCmakeConfigPkg, + vcpkgCmakePkg, + vcpkgCmakePkg, + sampleLibPkg, + } + + // relationships require IDs to be set to be sorted similarly + for i := range expectedPkgs { + expectedPkgs[i].SetID() + } + pkg.Sort(expectedPkgs) + + expectedRelationships := []artifact.Relationship{ + { + From: fmtPkg, + To: helloPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: vcpkgCmakePkg, + To: fmtPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: vcpkgCmakeConfigPkg, + To: fmtPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: sampleLibPkg, + To: helloPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: fmtPkg, + To: sampleLibPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: vcpkgCmakePkg, + To: sampleLibPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: vcpkgCmakeConfigPkg, + To: sampleLibPkg, + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + } + + catalogerCfg := CatalogerConfig{ + VcpkgAllowGitClone: false, + } + pkgtest.TestCataloger(t, fixture, NewVcpkgManifestCataloger(catalogerCfg), expectedPkgs, expectedRelationships) +} + +// the registry the helloworld fixture pins; see testdata/vcpkg/helloworld/vcpkg-configuration.json +const vcpkgTestRegistryURL = "https://github.com/anchore/vcpkg-test-fixture" + +// useLocalVcpkgRegistryCache points syft's cache manager at a gitignored testdata cache and ensures a local +// clone of the vcpkg test registry exists there, so the resolver resolves it offline via git.PlainOpen +// (getVcpkgGitCachePath resolves to "/../vcpkg/registries/git"). +func useLocalVcpkgRegistryCache(t *testing.T) { + t.Helper() + + cacheRoot := filepath.Join("testdata", "cache") + syftCacheRoot := filepath.Join(cacheRoot, "syft") + registryGitDir := filepath.Join(cacheRoot, "vcpkg", "registries", "git") + + prepareVcpkgRegistryCache(t, registryGitDir) + + mgr, err := cache.NewFromDir(syftCacheRoot, 24*time.Hour) + require.NoError(t, err) + prev := cache.GetManager() + cache.SetManager(mgr) + t.Cleanup(func() { cache.SetManager(prev) }) +} + +// prepareVcpkgRegistryCache clones the vcpkg test registry into gitDir if a valid clone is not already present. +// the clone happens once (first run, requires network); later runs reuse it offline. +func prepareVcpkgRegistryCache(t *testing.T, gitDir string) { + t.Helper() + + if _, err := git.PlainOpen(gitDir); err == nil { + return // valid cache already present + } + // clear any partial/stale state left by an interrupted run + require.NoError(t, os.RemoveAll(gitDir)) + require.NoError(t, os.MkdirAll(filepath.Dir(gitDir), 0o755)) + + // clone into a temp sibling then atomically move into place so an interrupted or concurrent run can't + // observe a half-written cache + tmp, err := os.MkdirTemp(filepath.Dir(gitDir), "clone-") + require.NoError(t, err) + defer os.RemoveAll(tmp) + + cloneTarget := filepath.Join(tmp, "git") + if _, err := git.PlainClone(cloneTarget, false, &git.CloneOptions{URL: vcpkgTestRegistryURL}); err != nil { + t.Skipf("vcpkg registry cache is not present and could not be prepared (first run requires network): %v", err) + } + require.NoError(t, os.Rename(cloneTarget, gitDir)) +} diff --git a/syft/pkg/cataloger/cpp/testdata/.gitignore b/syft/pkg/cataloger/cpp/testdata/.gitignore new file mode 100644 index 000000000..5bc1ec057 --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/.gitignore @@ -0,0 +1,2 @@ +/cache/ +test-observations.json diff --git a/syft/pkg/cataloger/cpp/testdata/README.md b/syft/pkg/cataloger/cpp/testdata/README.md index e1311bc2c..35456cf73 100644 --- a/syft/pkg/cataloger/cpp/testdata/README.md +++ b/syft/pkg/cataloger/cpp/testdata/README.md @@ -1,8 +1,10 @@ -# Conan test data +# C++ test data -This folder contains the test data for the Conan package manager. +This folder contains the test data for the Conan and Vcpkg package managers. -## conan.lock +## Conan test data + +### conan.lock The conan lock file is created in the following way. @@ -14,4 +16,9 @@ This is necessary to verify that the dependency tree is properly parsed. `sed -i 's|mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39|mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39|g' conan.lock` 3. Manually delete the package id and prev from tinyxml2 entry to test conan lock parsing if they are missing: `sed -i 's|\"package_id\": \"6557f18ca99c0b6a233f43db00e30efaa525e27e\",||g' conan.lock` - `sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||g' conan.lock` \ No newline at end of file + `sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||g' conan.lock` + +## Vcpkg test data + +vcpkg/helloworld contains a vcpkg project that contains only the files necessary to retrieve all of the manifests for dependencies listed +in the vcpkg.json file. These manifests exist in a remote vcpkg git registry located at https://github.com/anchore/vcpkg-test-fixture diff --git a/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json @@ -0,0 +1 @@ +{} diff --git a/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json @@ -0,0 +1 @@ +{} diff --git a/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json @@ -0,0 +1 @@ +{} diff --git a/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp new file mode 100644 index 000000000..0fbad3524 --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + fmt::print("Hello World!\n"); + return 0; +} diff --git a/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json new file mode 100644 index 000000000..43f7a635d --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json @@ -0,0 +1,7 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3", + "repository": "https://github.com/anchore/vcpkg-test-fixture" + } +} diff --git a/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json new file mode 100644 index 000000000..5d2f1c9dc --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json @@ -0,0 +1,10 @@ +{ + "name": "hello", + "version": "0.1.0", + "dependencies": [ + { + "name": "fmt" + }, + "vcpkg-sample-library" + ] +} diff --git a/syft/pkg/type.go b/syft/pkg/type.go index b7155bdb6..b4d42853f 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -52,6 +52,7 @@ const ( SwiftPkg Type = "swift" SwiplPackPkg Type = "swiplpack" TerraformPkg Type = "terraform" + VcpkgPkg Type = "vcpkg" WordpressPluginPkg Type = "wordpress-plugin" HomebrewPkg Type = "homebrew" AppleAppBundlePkg Type = "apple-app-bundle" @@ -98,6 +99,7 @@ var AllPkgs = []Type{ SwiftPkg, SwiplPackPkg, TerraformPkg, + VcpkgPkg, WordpressPluginPkg, HomebrewPkg, AppleAppBundlePkg, @@ -174,6 +176,8 @@ func (t Type) PackageURLType() string { return "swiplpack" case TerraformPkg: return "terraform" + case VcpkgPkg: + return "vcpkg" case WordpressPluginPkg: return "wordpress-plugin" case HomebrewPkg: @@ -262,6 +266,8 @@ func TypeByName(name string) Type { return SwiplPackPkg case "terraform": return TerraformPkg + case "vcpkg": + return VcpkgPkg case "wordpress-plugin": return WordpressPluginPkg case "homebrew": diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 0e4201706..514d06b13 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -163,6 +163,7 @@ func TestTypeFromPURL(t *testing.T) { expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently expectedTypes.Remove(string(AppleAppBundlePkg)) // no standard purl type for apple app bundles expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package + expectedTypes.Remove(string(VcpkgPkg)) for _, test := range tests { t.Run(string(test.expected), func(t *testing.T) { diff --git a/syft/pkg/vcpkg.go b/syft/pkg/vcpkg.go new file mode 100644 index 000000000..165f846f1 --- /dev/null +++ b/syft/pkg/vcpkg.go @@ -0,0 +1,35 @@ +package pkg + +type VcpkgRegistryKind string + +const ( + FileSystem VcpkgRegistryKind = "filesystem" + Git VcpkgRegistryKind = "git" + Builtin VcpkgRegistryKind = "builtin" +) + +// used for metadata. Includes summary of data found in manifest (vcpkg.json file) relevant to just that vcpkg +type VcpkgManifest struct { + Description []string `json:"description,omitempty"` + Documentation string `json:"documentation,omitempty"` + FullVersion string `json:"full-version"` + Version string `json:"version"` + PortVersion int `json:"port-version"` + Maintainers []string `json:"maintainers,omitempty"` + Name string `json:"name"` + Supports string `json:"supports,omitempty"` + // to show where it came from + Registry *VcpkgRegistryEntry `json:"registry,omitempty"` + // found by looking at build folder to find target. ex. "x64-linux" + Triplet string `json:"triplet,omitempty"` +} + +// Matches definition of Vcpkg "Registry". https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-configuration-json#registry +type VcpkgRegistryEntry struct { + Baseline string `json:"baseline,omitempty"` + Kind VcpkgRegistryKind `json:"kind"` + Packages []string `json:"packages,omitempty"` + Path string `json:"path,omitempty"` + Reference string `json:"reference,omitempty"` + Repository string `json:"repository,omitempty"` +} From df1fc98e7aa861e39b34e0798aeaace028e28728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:26:04 +0000 Subject: [PATCH 038/168] chore(deps): bump anchore/workflows/.github/workflows/codeql.yaml (#5032) Bumps [anchore/workflows/.github/workflows/codeql.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0. - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4) --- updated-dependencies: - dependency-name: anchore/workflows/.github/workflows/codeql.yaml dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 6384d5959..022c2b9cb 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -13,7 +13,7 @@ permissions: {} jobs: analyze: name: Analyze - uses: anchore/workflows/.github/workflows/codeql.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 + uses: anchore/workflows/.github/workflows/codeql.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0 permissions: security-events: write packages: read From 5a7a72b53e10074cb3c210b9d6995203dc5ad013 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:26:27 +0000 Subject: [PATCH 039/168] chore(deps): bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 (#5030) Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.6 to 0.5.7. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/5f14fd08f7cf1cb1609c1e344975f152c7ee938d...192e21d79ab29983730a13d1382995c2307fbcaa) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.5.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/validate-github-actions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index d4c38eb98..b6a492f25 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: "Run zizmor" - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 with: # there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default) advanced-security: true From 832d96c1dd75c083e6256742c128d905ec8b457a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:26:37 +0000 Subject: [PATCH 040/168] chore(deps): bump actions/cache in /.github/actions/bootstrap (#5035) Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.1.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/bootstrap/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 26067046f..f32bd0f7d 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -60,7 +60,7 @@ runs: # ORAS cache: restore + save on the default branch of the canonical repo only. - name: Restore and save ORAS cache from github actions if: ${{ inputs.download-test-fixture-cache == 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.repository.fork == false }} - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ github.workspace }}/.tmp/oras-cache key: ${{ inputs.cache-key-prefix }}-oras-cache From 4eb61fc104bc4a09d6b5fb19d4e606d3bd1adf5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:26:50 +0000 Subject: [PATCH 041/168] chore(deps): bump anchore/workflows/.github/workflows/check-gate.yaml (#5034) Bumps [anchore/workflows/.github/workflows/check-gate.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0. - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4) --- updated-dependencies: - dependency-name: anchore/workflows/.github/workflows/check-gate.yaml dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b282397d4..42b9086d1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -41,7 +41,7 @@ jobs: permissions: contents: read checks: read # required for getting the status of specific check names - uses: anchore/workflows/.github/workflows/check-gate.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 + uses: anchore/workflows/.github/workflows/check-gate.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0 with: # these are checks that should be run on pull-request and merges to main. # we do NOT want to kick off a release if these have not been verified on main. From f2c9463e76fc96d6438e6ae7d658dd9266010faf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:30:15 +0000 Subject: [PATCH 042/168] chore(deps): bump anchore/go-make/.github/actions/setup (#5036) Bumps [anchore/go-make/.github/actions/setup](https://github.com/anchore/go-make) from 0.6.0 to 0.8.0. - [Release notes](https://github.com/anchore/go-make/releases) - [Commits](https://github.com/anchore/go-make/compare/39fe5f71112d4dceb3ff0a92a40f272f067fc457...430e2175bb166bfd138ef75466adbcc8ddab875f) --- updated-dependencies: - dependency-name: anchore/go-make/.github/actions/setup dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/bootstrap/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index f32bd0f7d..d9e3be414 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -30,7 +30,7 @@ runs: using: "composite" steps: - name: Setup go + go-make tooling - uses: anchore/go-make/.github/actions/setup@39fe5f71112d4dceb3ff0a92a40f272f067fc457 # v0.6.0 + uses: anchore/go-make/.github/actions/setup@430e2175bb166bfd138ef75466adbcc8ddab875f # v0.8.0 with: go-version: ${{ inputs.go-version }} cache-key-prefix: ${{ inputs.cache-key-prefix }} From 312f2532d0b0ca6a731a87f22ca4910aa8d001d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:30:45 +0000 Subject: [PATCH 043/168] chore(deps): bump modernc.org/sqlite from 1.51.0 to 1.53.0 (#5039) Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.51.0 to 1.53.0. - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.51.0...v1.53.0) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 74adff89d..f180f961b 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.46.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.51.0 + modernc.org/sqlite v1.53.0 ) require ( @@ -324,7 +324,7 @@ require ( google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - modernc.org/libc v1.72.3 // indirect + modernc.org/libc v1.73.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 5b38989f4..c259ce2c5 100644 --- a/go.sum +++ b/go.sum @@ -1513,20 +1513,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= -modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= -modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= -modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= -modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1535,8 +1535,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U= -modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From f6bf15bb3c1d8799c9fea06c0119f7bf69046847 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:31:03 +0000 Subject: [PATCH 044/168] chore(deps): bump golang.org/x/tools from 0.46.0 to 0.47.0 (#5038) Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.46.0 to 0.47.0. - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.46.0...v0.47.0) --- updated-dependencies: - dependency-name: golang.org/x/tools dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f180f961b..5fc0388f1 100644 --- a/go.mod +++ b/go.mod @@ -96,7 +96,7 @@ require ( golang.org/x/mod v0.37.0 golang.org/x/net v0.56.0 golang.org/x/time v0.15.0 - golang.org/x/tools v0.46.0 + golang.org/x/tools v0.47.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.53.0 ) diff --git a/go.sum b/go.sum index c259ce2c5..e902f2d3c 100644 --- a/go.sum +++ b/go.sum @@ -1315,8 +1315,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From ee48c01c06b0c3ce4da53d0eea1983ba1ceec3cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:31:51 +0000 Subject: [PATCH 045/168] chore(deps): bump actions/cache/restore in /.github/actions/bootstrap (#5037) Bumps [actions/cache/restore](https://github.com/actions/cache) from 5.0.5 to 6.1.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9) --- updated-dependencies: - dependency-name: actions/cache/restore dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/bootstrap/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index d9e3be414..aad6d6090 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -52,7 +52,7 @@ runs: # ORAS cache: restore-only on non-default branches / forks - name: Restore ORAS cache from github actions if: ${{ inputs.download-test-fixture-cache == 'true' && (github.ref != format('refs/heads/{0}', github.event.repository.default_branch) || github.event.repository.fork == true) }} - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ github.workspace }}/.tmp/oras-cache key: ${{ inputs.cache-key-prefix }}-oras-cache From 6f5c83a5585326b4a50d9bc0b178bd6991763535 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:42:48 +0000 Subject: [PATCH 046/168] chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#5024) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- .github/workflows/validate-github-actions.yaml | 2 +- .github/workflows/validations.yaml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 42b9086d1..5d13d2565 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -70,7 +70,7 @@ jobs: packages: write # required for publishing release artifacts to GitHub packages id-token: write # required for keyless signing (cosign/sigstore OIDC) steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: fetch-depth: 0 persist-credentials: true diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index b6a492f25..6aaa5b82c 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -20,7 +20,7 @@ jobs: contents: read security-events: write # for uploading SARIF results steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index e7eea1288..c520a05b7 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -28,7 +28,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false @@ -50,7 +50,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false @@ -72,7 +72,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false @@ -99,7 +99,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false @@ -129,7 +129,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false @@ -173,7 +173,7 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false @@ -207,7 +207,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: persist-credentials: false From 8c0d21e147b814cbfed3daad0406ef3bfcfb422b Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 6 Jul 2026 10:56:45 -0400 Subject: [PATCH 047/168] chore(deps): update CPE dictionary index (#5042) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 439 +++++++++++++++++- 1 file changed, 432 insertions(+), 7 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 22f6e3ef1..1d7a1dd69 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -37,6 +37,9 @@ "github.com/charmbracelet/soft-serve": [ "cpe:2.3:a:charm:soft_serve:*:*:*:*:*:go:*:*" ], + "github.com/cloudflare/circl": [ + "cpe:2.3:a:cloudflare:circl:*:*:*:*:*:go:*:*" + ], "github.com/containers/podman": [ "cpe:2.3:a:podman_project:podman:*:*:*:*:*:*:*:*" ], @@ -61,9 +64,18 @@ "github.com/free5gc/amf": [ "cpe:2.3:a:free5gc:amf:*:*:*:*:*:go:*:*" ], + "github.com/free5gc/bsf": [ + "cpe:2.3:a:free5gc:bsf:*:*:*:*:*:go:*:*" + ], "github.com/free5gc/go-upf": [ "cpe:2.3:a:free5gc:go-upf:*:*:*:*:*:go:*:*" ], + "github.com/free5gc/nef": [ + "cpe:2.3:a:free5gc:nef:*:*:*:*:*:go:*:*" + ], + "github.com/free5gc/nrf": [ + "cpe:2.3:a:free5gc:nrf:*:*:*:*:*:go:*:*" + ], "github.com/free5gc/smf": [ "cpe:2.3:a:free5gc:smf:*:*:*:*:*:go:*:*" ], @@ -106,6 +118,12 @@ "github.com/hashicorp/go-slug": [ "cpe:2.3:a:hashicorp:go-slug:*:*:*:*:*:*:*:*" ], + "github.com/jackc/pgx": [ + "cpe:2.3:a:jackc:pgx:*:*:*:*:*:go:*:*" + ], + "github.com/jackc/pgx/v5": [ + "cpe:2.3:a:jackc:pgx:*:*:*:*:*:go:*:*" + ], "github.com/jumpserver/koko/pkg/koko": [ "cpe:2.3:a:fit2cloud:koko:*:*:*:*:*:go:*:*" ], @@ -145,6 +163,12 @@ "github.com/ntbosscher/gobase": [ "cpe:2.3:a:gobase_project:gobase:*:*:*:*:*:go:*:*" ], + "github.com/open-telemetry/opentelemetry-collector-contrib": [ + "cpe:2.3:a:opentelemetry:opentelemetry_collector_contrib:*:*:*:*:*:go:*:*" + ], + "github.com/pocketbase/pocketbase": [ + "cpe:2.3:a:pocketbase:pocketbase:*:*:*:*:*:go:*:*" + ], "github.com/proglottis/gpgme": [ "cpe:2.3:a:gpgme_project:gpgme:*:*:*:*:*:go:*:*" ], @@ -163,6 +187,9 @@ "github.com/satori/go.uuid": [ "cpe:2.3:a:satori:uuid:*:*:*:*:*:go:*:*" ], + "github.com/shamaton/msgpack/v3": [ + "cpe:2.3:a:shamaton:msgpack:*:*:*:*:*:go:*:*" + ], "github.com/sigstore/gitsign": [ "cpe:2.3:a:sigstore:gitsign:*:*:*:*:*:go:*:*" ], @@ -193,14 +220,21 @@ "github.com/whilp/git-urls": [ "cpe:2.3:a:git-urls_project:git-urls:*:*:*:*:*:go:*:*" ], + "github.com/xddxdd/bird-lg-go": [ + "cpe:2.3:a:xddxdd:bird-lg-go:*:*:*:*:*:go:*:*" + ], "github.com/yuin/goldmark": [ "cpe:2.3:a:yuin:goldmark:*:*:*:*:*:go:*:*" ], "go.opentelemetry.io/otel": [ "cpe:2.3:a:opentelemetry:opentelemetry:*:*:*:*:*:go:*:*" ], + "go.opentelemetry.io/otel/schema": [ + "cpe:2.3:a:opentelemetry:telemetry_schema_files:*:*:*:*:*:go:*:*" + ], "golang.org/x/crypto": [ - "cpe:2.3:a:go:ssh:*:*:*:*:*:go:*:*" + "cpe:2.3:a:go:ssh:*:*:*:*:*:go:*:*", + "cpe:2.3:a:golang:crypto:*:*:*:*:*:go:*:*" ], "golang.org/x/crypto/ssh": [ "cpe:2.3:a:golang:package_ssh:*:*:*:*:*:*:*:*", @@ -217,6 +251,7 @@ "cpe:2.3:a:golang:tiff:*:*:*:*:*:go:*:*" ], "golang.org/x/net": [ + "cpe:2.3:a:golang:net:*:*:*:*:*:go:*:*", "cpe:2.3:a:golang:networking:*:*:*:*:*:go:*:*" ], "golang.org/x/net/html": [ @@ -673,6 +708,9 @@ "covcomplplot": [ "cpe:2.3:a:jenkins:coverage\\/complexity_scatter_plot:*:*:*:*:*:jenkins:*:*" ], + "coverage": [ + "cpe:2.3:a:jenkins:coverage:*:*:*:*:*:jenkins:*:*" + ], "coverity": [ "cpe:2.3:a:jenkins:coverity:*:*:*:*:*:jenkins:*:*" ], @@ -863,6 +901,9 @@ "eggplant": [ "cpe:2.3:a:jenkins:eggplant:*:*:*:*:*:jenkins:*:*" ], + "eggplant-runner": [ + "cpe:2.3:a:jenkins:eggplant_runner:*:*:*:*:*:jenkins:*:*" + ], "elastest": [ "cpe:2.3:a:jenkins:elastest:*:*:*:*:*:jenkins:*:*" ], @@ -903,6 +944,9 @@ "extended-choice-parameter": [ "cpe:2.3:a:jenkins:extended_choice_parameter:*:*:*:*:*:jenkins:*:*" ], + "extensible-choice-parameter": [ + "cpe:2.3:a:jenkins:extensible_choice_parameter:*:*:*:*:*:jenkins:*:*" + ], "extensivetesting": [ "cpe:2.3:a:jenkins:extensive_testing:*:*:*:*:*:jenkins:*:*" ], @@ -1032,6 +1076,9 @@ "github-pr-coverage-status": [ "cpe:2.3:a:jenkins:github_pull_request_coverage_status:*:*:*:*:*:jenkins:*:*" ], + "github-pullrequest": [ + "cpe:2.3:a:kostyasha:github_integration:*:*:*:*:*:jenkins:*:*" + ], "gitlab": [ "cpe:2.3:a:jenkins:gitlab:*:*:*:*:*:jenkins:*:*" ], @@ -1184,6 +1231,9 @@ "jenkins-cloudformation": [ "cpe:2.3:a:jenkins:jenkins-cloudformation-plugin:*:*:*:*:*:jenkins:*:*" ], + "jenkins-multijob": [ + "cpe:2.3:a:jenkins:multijob:*:*:*:*:*:jenkins:*:*" + ], "jenkins-multijob-plugin": [ "cpe:2.3:a:jenkins:multijob:*:*:*:*:*:jenkins:*:*" ], @@ -1285,6 +1335,9 @@ "lambdatest-automation": [ "cpe:2.3:a:jenkins:lambdatest-automation:*:*:*:*:*:jenkins:*:*" ], + "ldap": [ + "cpe:2.3:a:jenkins:ldap:*:*:*:*:*:jenkins:*:*" + ], "libvirt-slave": [ "cpe:2.3:a:jenkins:libvirt_agents:*:*:*:*:*:jenkins:*:*", "cpe:2.3:a:jenkins:libvirt_slaves:*:*:*:*:*:jenkins:*:*" @@ -1385,6 +1438,9 @@ "maven-release-cascade": [ "cpe:2.3:a:barchart:maven_cascade_release:*:*:*:*:*:jenkins:*:*" ], + "mcp-server": [ + "cpe:2.3:a:jenkins:mcp_server:*:*:*:*:*:jenkins:*:*" + ], "meliora-testlab": [ "cpe:2.3:a:jenkins:meliora_testlab:*:*:*:*:*:jenkins:*:*", "cpe:2.3:a:melioratestlab:melioratestlab:*:*:*:*:*:jenkins:*:*" @@ -1558,6 +1614,9 @@ "pipeline-githubnotify-step": [ "cpe:2.3:a:jenkins:pipeline_github_notify_step:*:*:*:*:*:jenkins:*:*" ], + "pipeline-groovy-lib": [ + "cpe:2.3:a:jenkins:pipeline\\:_groovy_libraries:*:*:*:*:*:*:*:*" + ], "pipeline-input-step": [ "cpe:2.3:a:jenkins:pipeline\\:_input_step:*:*:*:*:*:jenkins:*:*", "cpe:2.3:a:jenkins:pipeline\\:input_step:*:*:*:*:*:jenkins:*:*" @@ -1568,6 +1627,9 @@ "pipeline-model-definition": [ "cpe:2.3:a:jenkins:pipeline\\:_declarative:*:*:*:*:*:jenkins:*:*" ], + "pipeline-reporter-by-redpen": [ + "cpe:2.3:a:jenkins:redpen_-_pipeline_reporter_for_jira:*:*:*:*:*:jenkins:*:*" + ], "pipeline-restful-api": [ "cpe:2.3:a:jenkins:pipeline_restful_api:*:*:*:*:*:jenkins:*:*" ], @@ -2335,6 +2397,9 @@ "@backstage/techdocs-common": [ "cpe:2.3:a:linuxfoundation:\\@backstage\\/techdocs-common:*:*:*:*:*:node.js:*:*" ], + "@beproduct/nestjs-auth": [ + "cpe:2.3:a:beproduct:beproduct\\/nestjs-auth:*:*:*:*:*:node.js:*:*" + ], "@better-auth/oauth-provider": [ "cpe:2.3:a:better-auth:better-auth\\/oauth-provider:*:*:*:*:*:node.js:*:*" ], @@ -2350,6 +2415,9 @@ "@chainsafe/libp2p-noise": [ "cpe:2.3:a:chainsafe:js-libp2p-noise:*:*:*:*:*:node.js:*:*" ], + "@chargeover/redoc": [ + "cpe:2.3:a:chargeover:chargeover\\/redoc:*:*:*:*:*:node.js:*:*" + ], "@ckeditor/ckeditor5-engine": [ "cpe:2.3:a:ckeditor:ckeditor5-engine:*:*:*:*:*:node.js:*:*" ], @@ -2374,12 +2442,18 @@ "@ckeditor/ckeditor5-widget": [ "cpe:2.3:a:ckeditor:ckeditor5-widget:*:*:*:*:*:node.js:*:*" ], + "@clerk/backend": [ + "cpe:2.3:a:clerk:clerk\\/backend:*:*:*:*:*:node.js:*:*" + ], "@coding-solo/godot-mcp": [ "cpe:2.3:a:coding-solo:godot_mcp:*:*:*:*:*:*:*:*" ], "@contentauth/c2pa-web": [ "cpe:2.3:a:adobe:c2pa-web:*:*:*:*:*:node.js:*:*" ], + "@continuedev/cli": [ + "cpe:2.3:a:continue:continue:*:*:*:*:*:node.js:*:*" + ], "@cookiex/deep": [ "cpe:2.3:a:cookiex-deep_project:cookiex-deep:*:*:*:*:*:node.js:*:*" ], @@ -2392,15 +2466,36 @@ "@diez/generation": [ "cpe:2.3:a:haikuforteams:diez:*:*:*:*:*:node.js:*:*" ], + "@draftlab/auth": [ + "cpe:2.3:a:matheuspergoli:draftlab\\/auth:*:*:*:*:*:node.js:*:*" + ], + "@draftlab/db": [ + "cpe:2.3:a:matheuspergoli:draftlab\\/db:*:*:*:*:*:node.js:*:*" + ], "@electron/packager": [ "cpe:2.3:a:electron:packager:*:*:*:*:*:node.js:*:*" ], + "@ensdomains/ens-contracts": [ + "cpe:2.3:a:ens.domains:ethereum_name_service:*:*:*:*:*:node.js:*:*" + ], "@evershop/evershop": [ "cpe:2.3:a:evershop:evershop:*:*:*:*:*:node.js:*:*" ], + "@fastify/accepts-serializer": [ + "cpe:2.3:a:fastify:fastify\\/accepts-serializer:*:*:*:*:*:node.js:*:*" + ], + "@fastify/express": [ + "cpe:2.3:a:fastify:fastify\\/express:*:*:*:*:*:node.js:*:*" + ], + "@fastify/http-proxy": [ + "cpe:2.3:a:fastify:fastify\\/http-proxy:*:*:*:*:*:node.js:*:*" + ], "@fastify/middie": [ "cpe:2.3:a:fastify:fastify\\/middie:*:*:*:*:*:node.js:*:*" ], + "@fastify/reply-from": [ + "cpe:2.3:a:fastify:reply-from:*:*:*:*:*:node.js:*:*" + ], "@fastly/js-compute": [ "cpe:2.3:a:fastly:js-compute:*:*:*:*:*:node.js:*:*" ], @@ -2452,6 +2547,9 @@ "@keystonejs/keystone": [ "cpe:2.3:a:keystonejs:keystone:*:*:*:*:*:node.js:*:*" ], + "@kilocode/cli": [ + "cpe:2.3:a:kilo:kilo_code_cli:*:*:*:*:*:node.js:*:*" + ], "@knight-lab/timelinejs": [ "cpe:2.3:a:northwestern:timelinejs:*:*:*:*:*:*:*:*" ], @@ -2464,6 +2562,18 @@ "@mattkrick/sanitize-svg": [ "cpe:2.3:a:sanitize-svg_project:sanitize-svg:*:*:*:*:*:node.js:*:*" ], + "@mesadev/rest": [ + "cpe:2.3:a:mesa:mesadev\\/rest:*:*:*:*:*:node.js:*:*" + ], + "@mesadev/saguaro": [ + "cpe:2.3:a:mesa:mesadev\\/saguaro:*:*:*:*:*:node.js:*:*" + ], + "@mesadev/sdk": [ + "cpe:2.3:a:mesa:mesadev\\/sdk:*:*:*:*:*:node.js:*:*" + ], + "@microsoft/live-share-canvas": [ + "cpe:2.3:a:microsoft:live_share_canvas:*:*:*:*:*:node.js:*:*" + ], "@misskey-dev/summaly": [ "cpe:2.3:a:misskey:summaly:*:*:*:*:*:node.js:*:*" ], @@ -2476,6 +2586,9 @@ "@nextcloud/dialogs": [ "cpe:2.3:a:nextcloud\\/dialogs_project:nextcloud\\/dialogs:*:*:*:*:*:node.js:*:*" ], + "@node-oauth/oauth2-server": [ + "cpe:2.3:a:node-oauth:node-oauth\\/oauth2-server:*:*:*:*:*:node.js:*:*" + ], "@node-saml/node-saml": [ "cpe:2.3:a:node_saml_project:node_saml:*:*:*:*:*:node.js:*:*" ], @@ -2488,6 +2601,15 @@ "@nuxt/devalue": [ "cpe:2.3:a:nuxtjs:\\@nuxt\\/devalue:*:*:*:*:*:node.js:*:*" ], + "@nuxt/nitro-server": [ + "cpe:2.3:a:nuxt:nuxt\\/nitro-server:*:*:*:*:*:node.js:*:*" + ], + "@nuxt/rspack-builder": [ + "cpe:2.3:a:nuxt:nuxt\\/rspack-builder:*:*:*:*:*:node.js:*:*" + ], + "@nuxt/webpack-builder": [ + "cpe:2.3:a:nuxt:nuxt\\/webpack-builder:*:*:*:*:*:node.js:*:*" + ], "@nyariv/sandboxjs": [ "cpe:2.3:a:nyariv:sandboxjs:*:*:*:*:*:node.js:*:*" ], @@ -2500,6 +2622,9 @@ "@opennextjs/cloudflare": [ "cpe:2.3:a:opennextjs:opennext_for_cloudflare:*:*:*:*:*:node.js:*:*" ], + "@opensearch-project/opensearch": [ + "cpe:2.3:a:linuxfoundation:opensearch:*:*:*:*:*:node.js:*:*" + ], "@openzeppelin/contracts": [ "cpe:2.3:a:openzeppelin:contracts:*:*:*:*:*:node.js:*:*" ], @@ -2522,6 +2647,12 @@ "@progfay/scrapbox-parser": [ "cpe:2.3:a:scrapbox-parser_project:scrapbox-parser:*:*:*:*:*:node.js:*:*" ], + "@rage-against-the-pixel/unity-cli": [ + "cpe:2.3:a:rageagainstthepixel:unity-cli:*:*:*:*:*:node.js:*:*" + ], + "@remix-run/server-runtime": [ + "cpe:2.3:a:shopify:remix-run\\/server-runtime:*:*:*:*:*:node.js:*:*" + ], "@rkesters/gnuplot": [ "cpe:2.3:a:gnuplot_project:gnuplot:*:*:*:*:*:node.js:*:*" ], @@ -2552,6 +2683,12 @@ "@strikeentco/set": [ "cpe:2.3:a:set_project:set:*:*:*:*:*:node.js:*:*" ], + "@supersurkhet/cli": [ + "cpe:2.3:a:abhishake1:supersurkhet\\/cli:*:*:*:*:*:node.js:*:*" + ], + "@supersurkhet/sdk": [ + "cpe:2.3:a:abhishake1:supersurkhet\\/sdk:*:*:*:*:*:node.js:*:*" + ], "@sveltejs/adapter-node": [ "cpe:2.3:a:svelte:adapter-node:*:*:*:*:*:node.js:*:*" ], @@ -2564,6 +2701,9 @@ "@tarojs/taro": [ "cpe:2.3:a:taro:taro:*:*:*:*:*:node.js:*:*" ], + "@taskflow-corp/cli": [ + "cpe:2.3:a:abhishake1:taskflow-corp\\/cli:*:*:*:*:*:node.js:*:*" + ], "@thi.ng/egf": [ "cpe:2.3:a:\\@thi.ng\\/egf_project:\\@thi.ng\\/egf:*:*:*:*:*:node.js:*:*" ], @@ -2576,6 +2716,9 @@ "@tiptap/extension-link": [ "cpe:2.3:a:tiptap:tiptap\\/extension-link:*:*:*:*:*:node.js:*:*" ], + "@tolka/cli": [ + "cpe:2.3:a:christianalares:tolka\\/cli:*:*:*:*:*:node.js:*:*" + ], "@tryghost/portal": [ "cpe:2.3:a:ghost:portal:*:*:*:*:*:node.js:*:*" ], @@ -2909,6 +3052,9 @@ "chatbyvista": [ "cpe:2.3:a:chatbyvista_project:chatbyvista:*:*:*:*:*:node.js:*:*" ], + "chrome-devtools-mcp": [ + "cpe:2.3:a:google:chrome-devtools-mcp:*:*:*:*:*:node.js:*:*" + ], "chrome-launcher": [ "cpe:2.3:a:google:chrome-launcher:*:*:*:*:*:node.js:*:*" ], @@ -2955,6 +3101,9 @@ "cmake": [ "cpe:2.3:a:cmake_project:cmake:*:*:*:*:*:node.js:*:*" ], + "cmux-agent-mcp": [ + "cpe:2.3:a:multiagentcognition:cmux-agent-mcp:*:*:*:*:*:node.js:*:*" + ], "co-cli-installer": [ "cpe:2.3:a:co-cli-installer_project:co-cli-installer:*:*:*:*:*:node.js:*:*" ], @@ -3161,7 +3310,7 @@ "cpe:2.3:a:deep.assign_project:deep.assign:*:*:*:*:*:node.js:*:*" ], "deephas": [ - "cpe:2.3:a:deephas_project:deephas:*:*:*:*:*:node.js:*:*" + "cpe:2.3:a:sharpred:deephas:*:*:*:*:*:node.js:*:*" ], "deeply": [ "cpe:2.3:a:deeply_project:deeply:*:*:*:*:*:node.js:*:*" @@ -3412,6 +3561,9 @@ "external-svg-loader": [ "cpe:2.3:a:shubhamjain:svg_loader:*:*:*:*:*:node.js:*:*" ], + "extract-zip": [ + "cpe:2.3:a:max-mapper:extract-zip:*:*:*:*:*:node.js:*:*" + ], "exxxxxxxxxxx": [ "cpe:2.3:a:exxxxxxxxxxx_project:exxxxxxxxxxx:*:*:*:*:*:node.js:*:*" ], @@ -3595,9 +3747,15 @@ "git-archive": [ "cpe:2.3:a:git-archive_project:git-archive:*:*:*:*:*:node.js:*:*" ], + "git-branch-selector": [ + "cpe:2.3:a:christianalares:git_branch_selector:*:*:*:*:*:node.js:*:*" + ], "git-dummy-commit": [ "cpe:2.3:a:git-dummy-commit_project:git-dummy-commit:*:*:*:*:*:node.js:*:*" ], + "git-git-git": [ + "cpe:2.3:a:christianalares:git-git-git:*:*:*:*:*:node.js:*:*" + ], "git-parse": [ "cpe:2.3:a:wayfair:git-parse:*:*:*:*:*:node.js:*:*" ], @@ -3828,6 +3986,9 @@ "http-proxy": [ "cpe:2.3:a:http-proxy_project:http-proxy:*:*:*:*:*:node.js:*:*" ], + "http-proxy-middleware": [ + "cpe:2.3:a:chimurai:http-proxy-middleware:*:*:*:*:*:*:*:*" + ], "http-proxy.js": [ "cpe:2.3:a:http-proxy.js_project:http-proxy.js:*:*:*:*:*:node.js:*:*" ], @@ -3864,6 +4025,18 @@ "i18n-node-angular": [ "cpe:2.3:a:i18n-node-angular_project:i18n-node-angular:*:*:*:*:*:node.js:*:*" ], + "i18next-fs-backend": [ + "cpe:2.3:a:i18next:i18next-fs-backend:*:*:*:*:*:node.js:*:*" + ], + "i18next-http-backend": [ + "cpe:2.3:a:i18next:i18next-http-backend:*:*:*:*:*:node.js:*:*" + ], + "i18next-http-middleware": [ + "cpe:2.3:a:i18next:i18next-http-middleware:*:*:*:*:*:node.js:*:*" + ], + "i18nextify": [ + "cpe:2.3:a:i18next:i18nextify:*:*:*:*:*:node.js:*:*" + ], "ibapi": [ "cpe:2.3:a:interactivebrokers:ibapi:*:*:*:*:*:node.js:*:*" ], @@ -3927,6 +4100,9 @@ "ip": [ "cpe:2.3:a:fedorindutny:ip:*:*:*:*:*:node.js:*:*" ], + "ip-address": [ + "cpe:2.3:a:beaugunderson:ip-address:*:*:*:*:*:node.js:*:*" + ], "ipip": [ "cpe:2.3:a:ipip_project:ipip:*:*:*:*:*:node.js:*:*" ], @@ -3986,7 +4162,7 @@ "cpe:2.3:a:joplin_project:joplin:*:*:*:*:*:node.js:*:*" ], "jose": [ - "cpe:2.3:a:jose_project:jose:*:*:*:*:*:node.js:*:*" + "cpe:2.3:a:panva:jose:*:*:*:*:*:node.js:*:*" ], "jpeg-js": [ "cpe:2.3:a:jpeg-js_project:jpeg-js:*:*:*:*:*:node.js:*:*" @@ -4022,6 +4198,9 @@ "js-given": [ "cpe:2.3:a:js-given_project:js-given:*:*:*:*:*:node.js:*:*" ], + "js-toml": [ + "cpe:2.3:a:sunnyadn:js-toml:*:*:*:*:*:node.js:*:*" + ], "js-yaml": [ "cpe:2.3:a:nodeca:js-yaml:*:*:*:*:*:node.js:*:*" ], @@ -4068,7 +4247,7 @@ "cpe:2.3:a:jsreport:jsreport:*:*:*:*:*:node.js:*:*" ], "jsrsasign": [ - "cpe:2.3:a:jsrsasign_project:jsrsasign:*:*:*:*:*:node.js:*:*" + "cpe:2.3:a:kjur:jsrsasign:*:*:*:*:*:node.js:*:*" ], "jstestdriver": [ "cpe:2.3:a:jstestdriver_project:jstestdriver:*:*:*:*:*:node.js:*:*" @@ -4299,6 +4478,9 @@ "madlib-object-utils": [ "cpe:2.3:a:springtree:madlib-object-utils:*:*:*:*:*:node.js:*:*" ], + "magicmirror": [ + "cpe:2.3:a:magicmirror:magicmirror:*:*:*:*:*:node.js:*:*" + ], "mailparser": [ "cpe:2.3:a:nodemailer:mailparser:*:*:*:*:*:node.js:*:*" ], @@ -4309,6 +4491,7 @@ "cpe:2.3:a:mapbox:mapbox.js:*:*:*:*:*:node.js:*:*" ], "mariadb": [ + "cpe:2.3:a:mariadb:mariadb:*:*:*:*:*:*:*:*", "cpe:2.3:a:mariadb:mariadb:*:*:*:*:*:node.js:*:*" ], "marionette-socket-host": [ @@ -4356,6 +4539,9 @@ "mc-kill-port": [ "cpe:2.3:a:mc-kill-port_project:mc-kill-port:*:*:*:*:*:node.js:*:*" ], + "mcp-from-openapi": [ + "cpe:2.3:a:frontmcp:mcp-from-openapi:*:*:*:*:*:node.js:*:*" + ], "mcp-server-kubernetes": [ "cpe:2.3:a:suyogs:mcp-server-kubernetes:*:*:*:*:*:node.js:*:*" ], @@ -4606,6 +4792,9 @@ "next-auth": [ "cpe:2.3:a:nextauth.js:next-auth:*:*:*:*:*:node.js:*:*" ], + "nextmove-mcp": [ + "cpe:2.3:a:christianalares:nextmove-mcp:*:*:*:*:*:node.js:*:*" + ], "nis-utils": [ "cpe:2.3:a:nis-utils_project:nis-utils:*:*:*:*:*:node.js:*:*" ], @@ -4722,6 +4911,9 @@ "node-stringbuilder": [ "cpe:2.3:a:magiclen:stringbuilder:*:*:*:*:*:node.js:*:*" ], + "node-tesseract-ocr": [ + "cpe:2.3:a:zapolnoch:tesseract_ocr:*:*:*:*:*:node.js:*:*" + ], "node-thulac": [ "cpe:2.3:a:geohey:node-thulac:*:*:*:*:*:node.js:*:*" ], @@ -5085,6 +5277,9 @@ "protobufjs": [ "cpe:2.3:a:protobufjs_project:protobufjs:*:*:*:*:*:node.js:*:*" ], + "protobufjs-cli": [ + "cpe:2.3:a:protobufjs_project:protobufjs-cli:*:*:*:*:*:node.js:*:*" + ], "proxy": [ "cpe:2.3:a:proxy_project:proxy:*:*:*:*:*:node.js:*:*" ], @@ -5263,6 +5458,9 @@ "s3-uploader": [ "cpe:2.3:a:s3-uploader_project:s3-uploader:*:*:*:*:*:node.js:*:*" ], + "safe-action": [ + "cpe:2.3:a:matheuspergoli:simple_type-safe_actions:*:*:*:*:*:node.js:*:*" + ], "safe-eval": [ "cpe:2.3:a:safe-eval_project:safe-eval:*:*:*:*:*:node.js:*:*" ], @@ -5594,7 +5792,6 @@ "cpe:2.3:a:storybook:storybook:*:*:*:*:*:node.js:*:*" ], "strapi": [ - "cpe:2.3:a:strapi:strapi:*:*:*:*:*:*:*:*", "cpe:2.3:a:strapi:strapi:*:*:*:*:*:node.js:*:*" ], "strider-sauce": [ @@ -5651,7 +5848,11 @@ ], "tar": [ "cpe:2.3:a:isaacs:tar:*:*:*:*:*:node.js:*:*", - "cpe:2.3:a:tar_project:tar:*:*:*:*:*:node.js:*:*" + "cpe:2.3:a:tar_project:tar:*:*:*:*:*:node.js:*:*", + "cpe:2.3:a:tar_project:tar:*:*:*:*:*:rust:*:*" + ], + "tarteaucitronjs": [ + "cpe:2.3:a:amauri:tarteaucitronjs:*:*:*:*:*:node.js:*:*" ], "teddy": [ "cpe:2.3:a:teddy_project:teddy:*:*:*:*:*:node.js:*:*" @@ -5698,6 +5899,9 @@ "tmock": [ "cpe:2.3:a:tmock_project:tmock:*:*:*:*:*:node.js:*:*" ], + "tmp": [ + "cpe:2.3:a:raszi:tmp:*:*:*:*:*:node.js:*:*" + ], "tmpl": [ "cpe:2.3:a:tmpl_project:tmpl:*:*:*:*:*:node.js:*:*" ], @@ -5755,6 +5959,9 @@ "tsup": [ "cpe:2.3:a:egoist:tsup:*:*:*:*:*:node.js:*:*" ], + "turbo-stream": [ + "cpe:2.3:a:turbo-stream:turbo_stream:*:*:*:*:*:node.js:*:*" + ], "ua-parser": [ "cpe:2.3:a:ua-parser_project:ua-parser:*:*:*:*:*:node.js:*:*" ], @@ -5866,6 +6073,12 @@ "vega-functions": [ "cpe:2.3:a:vega-functions_project:vega-functions:*:*:*:*:*:node.js:*:*" ], + "velocityjs": [ + "cpe:2.3:a:shepherdwind:velocity.js:*:*:*:*:*:node.js:*:*" + ], + "vercel": [ + "cpe:2.3:a:vercel:vercel:*:*:*:*:*:node.js:*:*" + ], "vite": [ "cpe:2.3:a:vitejs:vite:*:*:*:*:*:node.js:*:*" ], @@ -5903,6 +6116,9 @@ "webdriver-launcher": [ "cpe:2.3:a:webdriver-launcher_project:webdriver-launcher:*:*:*:*:*:node.js:*:*" ], + "webdriverio": [ + "cpe:2.3:a:openjsf:webdriverio:*:*:*:*:*:node.js:*:*" + ], "webdrvr": [ "cpe:2.3:a:webdrvr_project:webdrvr:*:*:*:*:*:node.js:*:*" ], @@ -5942,6 +6158,9 @@ "wifiscanner": [ "cpe:2.3:a:thingssdk:wifiscanner:*:*:*:*:*:node.js:*:*" ], + "willitmerge": [ + "cpe:2.3:a:dontkry:willitmerge:*:*:*:*:*:node.js:*:*" + ], "wincred": [ "cpe:2.3:a:wincred_project:wincred:*:*:*:*:*:*:*:*" ], @@ -6047,6 +6266,9 @@ "joomla/session": [ "cpe:2.3:a:joomla:session:*:*:*:*:*:*:*:*" ], + "mtdowling/jmespath.php": [ + "cpe:2.3:a:jmespath:jmespath:*:*:*:*:*:php:*:*" + ], "mustache/mustache": [ "cpe:2.3:a:mustache_project:mustache:*:*:*:*:*:php:*:*" ], @@ -6160,6 +6382,9 @@ "XML2Dict": [ "cpe:2.3:a:xml2dict_project:xml2dict:*:*:*:*:*:python:*:*" ], + "adx-mcp-server": [ + "cpe:2.3:a:pab1it0:azure_data_explorer_mcp_server:*:*:*:*:*:python:*:*" + ], "agixt": [ "cpe:2.3:a:agixt:agixt:*:*:*:*:*:*:*:*" ], @@ -6172,6 +6397,12 @@ "aniso8601": [ "cpe:2.3:a:aniso8601_project:aniso8601:*:*:*:*:*:*:*:*" ], + "apache-airflow-providers-amazon": [ + "cpe:2.3:a:apache:apache-airflow-providers-amazon:*:*:*:*:*:*:*:*" + ], + "apache-airflow-providers-cncf-kubernetes": [ + "cpe:2.3:a:apache:apache-airflow-providers-cncf-kubernetes:*:*:*:*:*:*:*:*" + ], "api-res-py": [ "cpe:2.3:a:api-res-py_project:api-res-py:*:*:*:*:*:python:*:*" ], @@ -6184,6 +6415,9 @@ "autobahn": [ "cpe:2.3:a:crossbar:autobahn:*:*:*:*:*:*:*:*" ], + "awslabs.aws-api-mcp-server": [ + "cpe:2.3:a:amazon:aws_api_mcp_server:*:*:*:*:*:python:*:*" + ], "azure-core": [ "cpe:2.3:a:microsoft:azure_core_shared_client_library:*:*:*:*:*:python:*:*" ], @@ -6220,6 +6454,9 @@ "certifi": [ "cpe:2.3:a:certifi:certifi:*:*:*:*:*:python:*:*" ], + "chromadb": [ + "cpe:2.3:a:trychroma:chromadb:*:*:*:*:*:python:*:*" + ], "claude-agent-sdk": [ "cpe:2.3:a:anthropic:claude_agent_sdk:*:*:*:*:*:python:*:*" ], @@ -6269,6 +6506,9 @@ "d8s-uuids": [ "cpe:2.3:a:democritus_uuids_project:democritus_uuids:*:*:*:*:*:python:*:*" ], + "dash-uploader": [ + "cpe:2.3:a:fohrloop:dash-uploader:*:*:*:*:*:python:*:*" + ], "datapizza-ai": [ "cpe:2.3:a:datapizza:datapizza_ai:*:*:*:*:*:*:*:*" ], @@ -6284,6 +6524,9 @@ "django-user-sessions": [ "cpe:2.3:a:django-user-sessions_project:django-user-sessions:*:*:*:*:*:*:*:*" ], + "docling": [ + "cpe:2.3:a:docling:docling:*:*:*:*:*:python:*:*" + ], "docling-core": [ "cpe:2.3:a:docling:docling-core:*:*:*:*:*:python:*:*" ], @@ -6350,6 +6593,9 @@ "guarddog": [ "cpe:2.3:a:datadoghq:guarddog:*:*:*:*:*:python:*:*" ], + "guardrails-ai": [ + "cpe:2.3:a:guardrailsai:guardrails_ai:*:*:*:*:*:python:*:*" + ], "hail": [ "cpe:2.3:a:hail:hail:*:*:*:*:*:python:*:*" ], @@ -6372,6 +6618,9 @@ "hyper-bump-it": [ "cpe:2.3:a:plannigan:hyper_bump_it:*:*:*:*:*:python:*:*" ], + "idna": [ + "cpe:2.3:a:kjd:internationalized_domain_names_in_applications:*:*:*:*:*:python:*:*" + ], "invenio-records": [ "cpe:2.3:a:inveniosoftware:invenio-records:*:*:*:*:*:*:*:*" ], @@ -6384,6 +6633,9 @@ "jwcrypto": [ "cpe:2.3:a:latchset:jwcrypto:*:*:*:*:*:*:*:*" ], + "kafka-python": [ + "cpe:2.3:a:dpkp:kafka-python:*:*:*:*:*:python:*:*" + ], "kedro": [ "cpe:2.3:a:linuxfoundation:kedro:*:*:*:*:*:python:*:*" ], @@ -6402,9 +6654,15 @@ "langchain-experimental": [ "cpe:2.3:a:langchain:langchain_experimental:*:*:*:*:*:python:*:*" ], + "langflow": [ + "cpe:2.3:a:langflow:langflow:*:*:*:*:*:*:*:*" + ], "langflow-base": [ "cpe:2.3:a:langflow:langflow-base:*:*:*:*:*:python:*:*" ], + "langgraph-checkpoint": [ + "cpe:2.3:a:langchain:langgraph-checkpoint:*:*:*:*:*:python:*:*" + ], "lerobot": [ "cpe:2.3:a:huggingface:lerobot:*:*:*:*:*:python:*:*" ], @@ -6438,12 +6696,21 @@ "mcp-memory-service": [ "cpe:2.3:a:doobidoo:mcp-memory-service:*:*:*:*:*:*:*:*" ], + "megatron-bridge": [ + "cpe:2.3:a:nvidia:nemo_megatron_bridge:*:*:*:*:*:*:*:*" + ], + "mistralai": [ + "cpe:2.3:a:mistral:mistralai:*:*:*:*:*:python:*:*" + ], "mltable": [ "cpe:2.3:a:microsoft:azure_machine_learning_software_development_kit:*:*:*:*:*:*:*:*" ], "mpxj": [ "cpe:2.3:a:mpxj:mpxj:*:*:*:*:*:python:*:*" ], + "nanobot-ai": [ + "cpe:2.3:a:nanobot:nanobot:*:*:*:*:*:python:*:*" + ], "nbconvert": [ "cpe:2.3:a:jupyter:nbconvert:*:*:*:*:*:python:*:*" ], @@ -6453,6 +6720,9 @@ "networkx": [ "cpe:2.3:a:python:networkx:*:*:*:*:*:*:*:*" ], + "nltk": [ + "cpe:2.3:a:nltk:nltk:*:*:*:*:*:*:*:*" + ], "novajoin": [ "cpe:2.3:a:python:novajoin:*:*:*:*:*:*:*:*" ], @@ -6579,6 +6849,9 @@ "pymdown-extensions": [ "cpe:2.3:a:facelessuser:pymdown_extensions:*:*:*:*:*:*:*:*" ], + "pymetasploit3": [ + "cpe:2.3:a:danmcinerney:pymetasploit3:*:*:*:*:*:*:*:*" + ], "pyo": [ "cpe:2.3:a:pyo_project:pyo:*:*:*:*:*:*:*:*" ], @@ -6591,6 +6864,9 @@ "pypolicyd-spf": [ "cpe:2.3:a:pypolicyd-spf_project:pypolicyd-spf:*:*:*:*:*:*:*:*" ], + "pyspector": [ + "cpe:2.3:a:parzivalhack:pyspector:*:*:*:*:*:python:*:*" + ], "python-dotenv": [ "cpe:2.3:a:saurabh-kumar:python-dotenv:*:*:*:*:*:python:*:*" ], @@ -6606,12 +6882,18 @@ "python-libnmap": [ "cpe:2.3:a:python-libnmap_project:python-libnmap:*:*:*:*:*:python:*:*" ], + "python-liquid": [ + "cpe:2.3:a:jg-rp:python_liquid:*:*:*:*:*:python:*:*" + ], "python-multipart": [ "cpe:2.3:a:fastapiexpert:python-multipart:*:*:*:*:*:python:*:*" ], "python-scciclient": [ "cpe:2.3:a:python-scciclient_project:python-scciclient:*:*:*:*:*:python:*:*" ], + "python-statemachine": [ + "cpe:2.3:a:fgmacedo:python_statemachine:*:*:*:*:*:python:*:*" + ], "pytorch-lightning": [ "cpe:2.3:a:lightningai:pytorch_lightning:*:*:*:*:*:python:*:*" ], @@ -6699,6 +6981,9 @@ "strawberry-graphql": [ "cpe:2.3:a:strawberry:strawberry_graphql:*:*:*:*:*:python:*:*" ], + "streamlink": [ + "cpe:2.3:a:streamlink:streamlink:*:*:*:*:*:python:*:*" + ], "tablib": [ "cpe:2.3:a:python:tablib:*:*:*:*:*:*:*:*" ], @@ -6744,6 +7029,9 @@ "vyper": [ "cpe:2.3:a:vyperlang:vyper:*:*:*:*:*:python:*:*" ], + "web3": [ + "cpe:2.3:a:apeworx:web3.py:*:*:*:*:*:python:*:*" + ], "wikifaces": [ "cpe:2.3:a:wikifaces_project:wikifaces:*:*:*:*:*:pypi:*:*" ], @@ -6836,6 +7124,9 @@ "commonmarker": [ "cpe:2.3:a:gjtorikian:commonmarker:*:*:*:*:*:ruby:*:*" ], + "concurrent-ruby": [ + "cpe:2.3:a:rubyconcurrency:concurrent_ruby:*:*:*:*:*:ruby:*:*" + ], "consul": [ "cpe:2.3:a:makandra:consul:*:*:*:*:*:ruby:*:*" ], @@ -6948,6 +7239,9 @@ "iodine": [ "cpe:2.3:a:boazsegev:iodine:*:*:*:*:*:ruby:*:*" ], + "jmespath": [ + "cpe:2.3:a:jmespath:jmespath:*:*:*:*:*:ruby:*:*" + ], "json": [ "cpe:2.3:a:json_project:json:*:*:*:*:*:ruby:*:*", "cpe:2.3:a:ruby-lang:json:*:*:*:*:*:ruby:*:*" @@ -7246,6 +7540,9 @@ ], "websocket-extensions": [ "cpe:2.3:a:websocket-extensions_project:websocket-extensions:*:*:*:*:*:ruby:*:*" + ], + "zlib": [ + "cpe:2.3:a:ruby-lang:zlib:*:*:*:*:*:ruby:*:*" ] }, "rust_crates": { @@ -7312,6 +7609,9 @@ "asn1_der": [ "cpe:2.3:a:asn1_der_project:asn1_der:*:*:*:*:*:*:*:*" ], + "astral-tokio-tar": [ + "cpe:2.3:a:astral:astral-tokio-tar:*:*:*:*:*:rust:*:*" + ], "async-h1": [ "cpe:2.3:a:rust-lang:async-h1:*:*:*:*:*:rust:*:*" ], @@ -7642,6 +7942,9 @@ "obstack": [ "cpe:2.3:a:obstack_project:obstack:*:*:*:*:*:rust:*:*" ], + "one2html": [ + "cpe:2.3:a:msiemens:one2html:*:*:*:*:*:rust:*:*" + ], "outer_cgi": [ "cpe:2.3:a:outer_cgi_project:outer_cgi:*:*:*:*:*:rust:*:*" ], @@ -7744,6 +8047,9 @@ "smallvec": [ "cpe:2.3:a:servo:smallvec:*:*:*:*:*:rust:*:*" ], + "spiral-rs": [ + "cpe:2.3:a:blyssprivacy:spiral-rs:*:*:*:*:*:rust:*:*" + ], "stack": [ "cpe:2.3:a:stack_project:stack:*:*:*:*:*:rust:*:*" ], @@ -7861,6 +8167,9 @@ "zebra-consensus": [ "cpe:2.3:a:zfnd:zebra-consensus:*:*:*:*:*:rust:*:*" ], + "zebra-network": [ + "cpe:2.3:a:zfnd:zebra-network:*:*:*:*:*:rust:*:*" + ], "zebra-rpc": [ "cpe:2.3:a:zfnd:zebra-rpc:*:*:*:*:*:rust:*:*" ], @@ -8807,6 +9116,9 @@ "aprils-super-functions-pack": [ "cpe:2.3:a:april\\'s_super_functions_pack_project:april\\'s_super_functions_pack:*:*:*:*:*:wordpress:*:*" ], + "aptivada-for-wp": [ + "cpe:2.3:a:aptivada:aptivada_for_wp:*:*:*:*:*:wordpress:*:*" + ], "apus-framework": [ "cpe:2.3:a:apusthemes:superio:*:*:*:*:*:wordpress:*:*" ], @@ -9408,6 +9720,9 @@ "bluet-keywords-tooltip-generator": [ "cpe:2.3:a:tooltipy:tooltipy:*:*:*:*:*:wordpress:*:*" ], + "bluetrait-event-viewer": [ + "cpe:2.3:a:bluetrait:blue_trait_event_viewer:*:*:*:*:*:wordpress:*:*" + ], "bmi-bmr-calculator": [ "cpe:2.3:a:bmi_bmr_calculator_project:bmi_bmr_calculator:*:*:*:*:*:wordpress:*:*" ], @@ -10081,6 +10396,9 @@ "christian-science-bible-lesson-subjects": [ "cpe:2.3:a:sharethepractice:christian_science_bible_lesson_subjects:*:*:*:*:*:wordpress:*:*" ], + "christmasify": [ + "cpe:2.3:a:cyberfoxdigital:christmasify\\!:*:*:*:*:*:wordpress:*:*" + ], "chronosly-events-calendar": [ "cpe:2.3:a:chronosly-events-calendar_project:chronosly-events-calendar:*:*:*:*:*:wordpress:*:*" ], @@ -10120,7 +10438,7 @@ "cpe:2.3:a:cm-wp:clearfy:*:*:*:*:*:wordpress:*:*" ], "clerkio": [ - "cpe:2.3:a:clerk:clerk.io:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:clerk.io:clerk.io:*:*:*:*:*:wordpress:*:*" ], "clever-fox": [ "cpe:2.3:a:nayrathemes:clever_fox:*:*:*:*:*:wordpress:*:*" @@ -10458,6 +10776,9 @@ "contact-form-manager": [ "cpe:2.3:a:xyzscripts:contact_form_manager:*:*:*:*:*:wordpress:*:*" ], + "contact-form-master": [ + "cpe:2.3:a:edmonparker:contact_form_master:*:*:*:*:*:wordpress:*:*" + ], "contact-form-multi": [ "cpe:2.3:a:bestwebsoft:contact_form_multi:*:*:*:*:*:wordpress:*:*" ], @@ -10651,6 +10972,9 @@ "corner-ad": [ "cpe:2.3:a:dwbooster:corner_ad:*:*:*:*:*:wordpress:*:*" ], + "coronavirus-covid-19-notice-message": [ + "cpe:2.3:a:gallagherwebsitedesign:coronavirus_\\(covid-19\\)_notice_message:*:*:*:*:*:wordpress:*:*" + ], "correos-express": [ "cpe:2.3:a:correosexpress_project:correosexpress:*:*:*:*:*:wordpress:*:*" ], @@ -10904,6 +11228,9 @@ "custom-field-for-wp-job-manager": [ "cpe:2.3:a:custom_field_for_wp_job_manager_project:custom_field_for_wp_job_manager:*:*:*:*:*:wordpress:*:*" ], + "custom-field-manager": [ + "cpe:2.3:a:f1logic:custom_field_manager:*:*:*:*:*:wordpress:*:*" + ], "custom-field-suite": [ "cpe:2.3:a:custom_field_suite_project:custom_field_suite:*:*:*:*:*:wordpress:*:*" ], @@ -11032,6 +11359,9 @@ "cysteme-finder": [ "cpe:2.3:a:cysteme:cysteme-finder:*:*:*:*:*:wordpress:*:*" ], + "cz-loan-management": [ + "cpe:2.3:a:contrive:cz_loan_management:*:*:*:*:*:wordpress:*:*" + ], "daext-autolinks-manager": [ "cpe:2.3:a:daext:autolinks_manager:*:*:*:*:*:wordpress:*:*" ], @@ -11203,6 +11533,9 @@ "directorypress-frontend": [ "cpe:2.3:a:designinvento:directorypress:*:*:*:*:*:wordpress:*:*" ], + "disable-auto-updates": [ + "cpe:2.3:a:exeebit:disable_auto_updates:*:*:*:*:*:wordpress:*:*" + ], "disable-comments": [ "cpe:2.3:a:disable_comments:disable_comments_project:*:*:*:*:*:wordpress:*:*" ], @@ -11242,6 +11575,12 @@ "dk-pricr-responsive-pricing-table": [ "cpe:2.3:a:wpdarko:responsive_pricing_table:*:*:*:*:*:wordpress:*:*" ], + "dl-robotstxt": [ + "cpe:2.3:a:dyadyalesha:dl_robots.txt:*:*:*:*:*:wordpress:*:*" + ], + "dl-verification": [ + "cpe:2.3:a:dyadyalesha:dl_verification:*:*:*:*:*:wordpress:*:*" + ], "dmsguestbook": [ "cpe:2.3:a:dmsguestbook_project:dmsguestbook:*:*:*:*:*:wordpress:*:*" ], @@ -11622,6 +11961,9 @@ "cpe:2.3:a:flowdee:easyazon:*:*:*:*:*:*:*:*", "cpe:2.3:a:getaawp:easyazon:*:*:*:*:*:wordpress:*:*" ], + "easyevent": [ + "cpe:2.3:a:dwalliance:easyevent:*:*:*:*:*:wordpress:*:*" + ], "easyjobs": [ "cpe:2.3:a:easy.jobs:easy.jobs:*:*:*:*:*:wordpress:*:*" ], @@ -12394,6 +12736,9 @@ "cpe:2.3:a:octolize:woocommerce_ups_shipping:*:*:*:*:free:wordpress:*:*", "cpe:2.3:a:octolize:woocommerce_ups_shipping:*:*:*:*:pro:wordpress:*:*" ], + "flickr-gallery": [ + "cpe:2.3:a:dancoulter:flickr_gallery:*:*:*:*:*:wordpress:*:*" + ], "flickr-justified-gallery": [ "cpe:2.3:a:flickr_justified_gallery_project:flickr_justified_gallery:*:*:*:*:*:wordpress:*:*" ], @@ -12540,6 +12885,9 @@ "formality": [ "cpe:2.3:a:giorgi:formality:*:*:*:*:*:wordpress:*:*" ], + "formassembly-web-forms": [ + "cpe:2.3:a:formassembly:wp-formassembly:*:*:*:*:*:wordpress:*:*" + ], "formbuilder": [ "cpe:2.3:a:formbuilder_project:formbuilder:*:*:*:*:*:wordpress:*:*" ], @@ -12677,6 +13025,9 @@ "furikake": [ "cpe:2.3:a:furikake_project:furikake:*:*:*:*:*:wordpress:*:*" ], + "fuse-social-floating-sidebar": [ + "cpe:2.3:a:daniyalahmedk:fuse_social_floating_sidebar:*:*:*:*:*:wordpress:*:*" + ], "fusedesk": [ "cpe:2.3:a:jeremyshapiro:fusedesk:*:*:*:*:*:wordpress:*:*" ], @@ -12831,6 +13182,9 @@ "gerryworks-post-by-mail": [ "cpe:2.3:a:gerryntabuhashe:gerryworks_post_by_mail:*:*:*:*:*:wordpress:*:*" ], + "gestpay-for-woocommerce": [ + "cpe:2.3:a:fabrick:gestpay_for_woocommerce:*:*:*:*:*:wordpress:*:*" + ], "get-custom-field-values": [ "cpe:2.3:a:get_custom_field_values_project:get_custom_field_values:*:*:*:*:*:wordpress:*:*" ], @@ -12861,6 +13215,9 @@ "gf-custom-style": [ "cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*" ], + "gf-zoho": [ + "cpe:2.3:a:crmperks:wp_gravity_forms_zoho_crm_and_bigin:*:*:*:*:*:wordpress:*:*" + ], "gg-woo-feed": [ "cpe:2.3:a:gutengeek:gg_woo_feed:*:*:*:*:*:wordpress:*:*" ], @@ -13706,6 +14063,9 @@ "inpost-gallery": [ "cpe:2.3:a:pluginus:inpost_gallery:*:*:*:*:*:wordpress:*:*" ], + "inprosysmedia-likes-dislikes-post": [ + "cpe:2.3:a:erumfaham:likes_and_dislikes:*:*:*:*:*:wordpress:*:*" + ], "insert-estimated-reading-time": [ "cpe:2.3:a:nigauri:insert_estimated_reading_time:*:*:*:*:*:wordpress:*:*" ], @@ -14597,6 +14957,9 @@ "magical-addons-for-elementor": [ "cpe:2.3:a:wpthemespace:magical_addons_for_elementor:*:*:*:*:*:wordpress:*:*" ], + "magicform": [ + "cpe:2.3:a:dcooperman:magicform:*:*:*:*:*:wordpress:*:*" + ], "mail-control": [ "cpe:2.3:a:instareza:mail_control:*:*:*:*:*:wordpress:*:*" ], @@ -14664,6 +15027,7 @@ "cpe:2.3:a:mainwp:mainwp_child:*:*:*:*:*:wordpress:*:*" ], "mainwp-child-reports": [ + "cpe:2.3:a:mainwp:mainwp_child:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:mainwp:mainwp_child_reports:*:*:*:*:*:wordpress:*:*" ], "majestic-support": [ @@ -14776,6 +15140,9 @@ "masterstudy-lms-learning-management-system": [ "cpe:2.3:a:stylemixthemes:masterstudy_lms:*:*:*:*:*:wordpress:*:*" ], + "masy-gallery": [ + "cpe:2.3:a:dineshrawat:masy_gallery:*:*:*:*:*:wordpress:*:*" + ], "material-design-for-contact-form-7": [ "cpe:2.3:a:material_design_for_contact_form_7_project:material_design_for_contact_form_7:*:*:*:*:*:wordpress:*:*" ], @@ -16013,6 +16380,9 @@ "personalize-woocommerce-cart-page": [ "cpe:2.3:a:najeebmedia:personalized_woocommerce_cart_page:*:*:*:*:*:wordpress:*:*" ], + "pet-manager": [ + "cpe:2.3:a:dianakcury:pet_manager:*:*:*:*:*:wordpress:*:*" + ], "peters-collaboration-e-mails": [ "cpe:2.3:a:peter\\'s_collaboration_e-mails_project:peter\\'s_collaboration_e-mails:*:*:*:*:*:wordpress:*:*" ], @@ -16090,6 +16460,9 @@ "phpfreechat": [ "cpe:2.3:a:frumph:phpfreechat:*:*:*:*:*:wordpress:*:*" ], + "phpinfo-wp": [ + "cpe:2.3:a:exeebit:phpinfo-wp:*:*:*:*:free:wordpress:*:*" + ], "pi-woocommerce-order-date-time-and-type": [ "cpe:2.3:a:piwebsolution:pi-woocommerce-order-date-time-and-type:*:*:*:*:*:wordpress:*:*" ], @@ -16142,6 +16515,9 @@ "plexx-elementor-extension": [ "cpe:2.3:a:themeworm:plexx_elementor_extension:*:*:*:*:*:wordpress:*:*" ], + "plugin-groups": [ + "cpe:2.3:a:davidcramer:plugin_groups:*:*:*:*:*:wordpress:*:*" + ], "plugin-logic": [ "cpe:2.3:a:plugin_logic_project:plugin_logic:*:*:*:*:*:wordpress:*:*" ], @@ -16943,6 +17319,9 @@ "cpe:2.3:a:redirection:redirection:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:redirection_project:redirection:*:*:*:*:*:wordpress:*:*" ], + "redirects": [ + "cpe:2.3:a:declaire:redirects:*:*:*:*:*:wordpress:*:*" + ], "redux-framework": [ "cpe:2.3:a:redux:gutenberg_template_library_\\\u0026_redux_framework:*:*:*:*:*:wordpress:*:*" ], @@ -17050,6 +17429,9 @@ "responsive-css-editor": [ "cpe:2.3:a:wpwox:responsive_css_editor:*:*:*:*:*:wordpress:*:*" ], + "responsive-food-and-drink-menu": [ + "cpe:2.3:a:corporatezen:responsive_food_and_drink_menu:*:*:*:*:*:wordpress:*:*" + ], "responsive-gallery-grid": [ "cpe:2.3:a:bdwm:responsive_gallery_grid:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:responsive_gallery_grid_project:responsive_gallery_grid:*:*:*:*:*:wordpress:*:*" @@ -17531,6 +17913,7 @@ "cpe:2.3:a:fabiantodt:private_post_share:*:*:*:*:*:wordpress:*:*" ], "share-buttons": [ + "cpe:2.3:a:artlosk:share_buttons:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:artlosk:social_share_buttons:*:*:*:*:*:wordpress:*:*" ], "share-on-diaspora": [ @@ -17690,6 +18073,9 @@ "simple-al-slider": [ "cpe:2.3:a:alexdtn:simple_al_slider:*:*:*:*:*:wordpress:*:*" ], + "simple-amazon-affiliate": [ + "cpe:2.3:a:duogeek:simple_amazon_affiliate:*:*:*:*:*:wordpress:*:*" + ], "simple-author-box": [ "cpe:2.3:a:webfactoryltd:simple_author_box:*:*:*:*:*:wordpress:*:*" ], @@ -18100,6 +18486,9 @@ "slide-anything": [ "cpe:2.3:a:simonpedge:slide_anything:*:*:*:*:*:wordpress:*:*" ], + "slidedeck-lite-for-wordpress": [ + "cpe:2.3:a:dtelepathy:slidedeck_1_lite_content_slider:*:*:*:*:*:wordpress:*:*" + ], "slidedeck2": [ "cpe:2.3:a:hbwsl:slidedeck_2:*:*:*:*:lite:wordpress:*:*" ], @@ -18169,6 +18558,9 @@ "smart-cookie-kit": [ "cpe:2.3:a:nicolamodugno:smart_cookie_kit:*:*:*:*:*:wordpress:*:*" ], + "smart-docs": [ + "cpe:2.3:a:archalj:smart_docs:*:*:*:*:*:wordpress:*:*" + ], "smart-donations": [ "cpe:2.3:a:rednao:donations_made_easy_-_smart_donations:*:*:*:*:*:wordpress:*:*" ], @@ -18488,6 +18880,9 @@ "stars-rating": [ "cpe:2.3:a:stars_rating_project:stars_rating:*:*:*:*:*:wordpress:*:*" ], + "starter-templates": [ + "cpe:2.3:a:fancywp:starter_templates:*:*:*:*:*:wordpress:*:*" + ], "startklar-elmentor-forms-extwidgets": [ "cpe:2.3:a:web-shop-host:startklar_elmentor_addons:*:*:*:*:*:wordpress:*:*" ], @@ -19237,6 +19632,9 @@ "twitter-cards-meta": [ "cpe:2.3:a:wpdeveloper:twitter_cards_meta:*:*:*:*:*:wordpress:*:*" ], + "twitter-follow": [ + "cpe:2.3:a:firecask:twitter_follow_button:*:*:*:*:*:wordpress:*:*" + ], "twitter-friends-widget": [ "cpe:2.3:a:twitter_friends_widget_project:twitter_friends_widget:*:*:*:*:*:wordpress:*:*" ], @@ -19476,6 +19874,9 @@ "upunzipper": [ "cpe:2.3:a:upunzipper_project:upunzipper:*:*:*:*:*:wordpress:*:*" ], + "url-media-uploader": [ + "cpe:2.3:a:apprhyme:url_media_uploader:*:*:*:*:*:wordpress:*:*" + ], "url-shortify": [ "cpe:2.3:a:kaizencoders:url_shortify:*:*:*:*:*:wordpress:*:*" ], @@ -19854,6 +20255,9 @@ "wds-multisite-aggregate": [ "cpe:2.3:a:webdevstudios:wds_multisite_aggregate:*:*:*:*:*:wordpress:*:*" ], + "we-testimonial-slider": [ + "cpe:2.3:a:wordpresteem:we_-_testimonial_slide:*:*:*:*:*:wordpress:*:*" + ], "weather-effect": [ "cpe:2.3:a:awplife:weather_effect:*:*:*:*:*:wordpress:*:*" ], @@ -20507,6 +20911,9 @@ "workreap": [ "cpe:2.3:a:amentotech:workreap:*:*:*:*:*:wordpress:*:*" ], + "wow-carousel-for-divi-lite": [ + "cpe:2.3:a:elegantthemes:divi_carousel:*:*:*:*:free:wordpress:*:*" + ], "wow-moodboard-lite": [ "cpe:2.3:a:wownmedia:wow_moodboard:*:*:*:*:lite:wordpress:*:*" ], @@ -20764,6 +21171,9 @@ "wp-crm": [ "cpe:2.3:a:usabilitydynamics:wp-crm:*:*:*:*:*:wordpress:*:*" ], + "wp-crm-system": [ + "cpe:2.3:a:wp-crm:wp-crm_system:*:*:*:*:*:wordpress:*:*" + ], "wp-crowdfunding": [ "cpe:2.3:a:themeum:wp_crowdfunding:*:*:*:*:*:wordpress:*:*" ], @@ -20879,6 +21289,9 @@ "wp-easy-contact": [ "cpe:2.3:a:emarketdesign:wp_easy_contact:*:*:*:*:*:wordpress:*:*" ], + "wp-easy-events": [ + "cpe:2.3:a:emarketdesign:event_rsvp_and_simple_event_management:*:*:*:*:*:wordpress:*:*" + ], "wp-easy-gallery": [ "cpe:2.3:a:plugingarden:wp_easy_gallery:*:*:*:*:*:wordpress:*:*" ], @@ -21243,6 +21656,9 @@ "cpe:2.3:a:3cx:live_chat:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:3cx:wp-live_chat:*:*:*:*:*:wordpress:*:*" ], + "wp-login-control": [ + "cpe:2.3:a:forsyspress:wp_login_control:*:*:*:*:*:wordpress:*:*" + ], "wp-login-security-and-history": [ "cpe:2.3:a:clogica:wp_login_security_and_history:*:*:*:*:*:wordpress:*:*" ], @@ -21391,6 +21807,9 @@ "wp-photo-text-slider-50": [ "cpe:2.3:a:gopiplus:wp_photo_text_slider_50:*:*:*:*:*:wordpress:*:*" ], + "wp-photonav": [ + "cpe:2.3:a:fmos:wp-photonav:*:*:*:*:*:wordpress:*:*" + ], "wp-phpmyadmin-extension": [ "cpe:2.3:a:puvox:wp_phpmyadmin:*:*:*:*:*:wordpress:*:*" ], @@ -22138,6 +22557,9 @@ "wpschoolpress": [ "cpe:2.3:a:igexsolutions:wpschoolpress:*:*:*:*:*:wordpress:*:*" ], + "wpshop": [ + "cpe:2.3:a:eoxia:wpshop_2:*:*:*:*:*:wordpress:*:*" + ], "wpshopgermany-it-recht-kanzlei": [ "cpe:2.3:a:maennchen1:wpshopgermany_it-recht_kanzlei:*:*:*:*:*:wordpress:*:*" ], @@ -22722,6 +23144,9 @@ "hello-agency": [ "cpe:2.3:a:cozythemes:hello_agency:*:*:*:*:*:wordpress:*:*" ], + "homey": [ + "cpe:2.3:a:favethemes:homey:*:*:*:*:*:wordpress:*:*" + ], "hotel-galaxy": [ "cpe:2.3:a:webdzier:hotel_galaxy:*:*:*:*:*:wordpress:*:*" ], From e4f5df936ced46274c610bb0e3fae234a0035e6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:59:47 -0400 Subject: [PATCH 048/168] chore(deps): bump anchore/workflows/.github/workflows/release-install-script.yaml (#5031) Bumps [anchore/workflows/.github/workflows/release-install-script.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0. - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4) --- updated-dependencies: - dependency-name: anchore/workflows/.github/workflows/release-install-script.yaml dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5d13d2565..90776175a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -119,7 +119,7 @@ jobs: if: ${{ always() && (needs.release.result == 'success' || github.event.inputs.phase == 'install-script-only') }} permissions: contents: read # required for the reusable workflow to check out the repo and publish the install script - uses: anchore/workflows/.github/workflows/release-install-script.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 + uses: anchore/workflows/.github/workflows/release-install-script.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0 with: tag: ${{ github.event.inputs.version }} secrets: From f1457f255424730655c8b024c1febfdd722be49f Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 6 Jul 2026 11:01:44 -0400 Subject: [PATCH 049/168] pin php apache fixture to debian snapshot (#5043) the plain version pin drifted out from under us: debian mirrors only keep the latest point release, so 8.2.31 became uninstallable once 8.2.32 shipped and apt pulled the unpinned transitive php8.2-cli at 8.2.32. pointing apt at snapshot.debian.org keeps the exact point release installable so the fixture (and its test expectations) stay reproducible. Signed-off-by: Alex Goodman --- .../cataloger/php/testdata/image-apache/Dockerfile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile b/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile index 4484d04bc..409677dec 100644 --- a/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile +++ b/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile @@ -1,5 +1,19 @@ FROM --platform=linux/amd64 httpd:2.4.63-bookworm AS builder +# pin apt to a debian snapshot so the exact php8.2 point release stays installable. +# a plain version pin drifts out from under us: debian's mirrors only keep the latest +# point release, so 8.2.31 disappears once 8.2.32 ships. snapshot.debian.org archives +# every version forever, which is what actually keeps the fixture (and its test +# expectations) reproducible. timestamp is any point where 8.2.31 was current +# (8.2.31: 2026-05-08, 8.2.32: 2026-07-04). +ARG SNAPSHOT=20260701T000000Z +RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/debian.sources && \ + printf '%s\n' \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${SNAPSHOT}/ bookworm main" \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${SNAPSHOT}/ bookworm-updates main" \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/${SNAPSHOT}/ bookworm-security main" \ + > /etc/apt/sources.list + # pin php8.2 packages to avoid version drift when debian publishes security updates ARG PHP_VERSION=8.2.31-1~deb12u1 RUN apt update -y && apt install -y \ From aebdf4bff444e8c0e16938203bd8371cd218b7e0 Mon Sep 17 00:00:00 2001 From: Synvoya Date: Tue, 7 Jul 2026 01:13:20 +1000 Subject: [PATCH 050/168] Fix mix.lock git/path deps mislabeled as hex.pm with bogus PURL (#5041) parseMixLock read the version from a hex-only token position and hardcoded a pkg:hex PURL, so git/path deps got a URL-fragment version and a false hex.pm PURL (false CVE matches). Branch on the source atom: git uses the commit SHA, path an empty version, and pkg:hex PURLs are emitted only for hex deps. Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com> Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com> --- syft/pkg/cataloger/elixir/package.go | 18 +++++++++-- syft/pkg/cataloger/elixir/parse_mix_lock.go | 30 ++++++++++++++++++- .../cataloger/elixir/parse_mix_lock_test.go | 28 +++++++++++++++++ syft/pkg/cataloger/elixir/testdata/mix.lock | 2 ++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/syft/pkg/cataloger/elixir/package.go b/syft/pkg/cataloger/elixir/package.go index 50cd3f28f..54fb21193 100644 --- a/syft/pkg/cataloger/elixir/package.go +++ b/syft/pkg/cataloger/elixir/package.go @@ -6,13 +6,18 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Package { +// newPackage builds a package from a mix.lock entry. source is the entry's +// source atom (`hex`, `git`, or `path`). Only hex entries are backed by the +// hex.pm registry, so only they receive a pkg:hex/ PURL; git/path entries get +// an empty PURL to avoid being asserted as hex.pm packages (which would produce +// false hex.pm vulnerability matches). +func newPackage(source string, d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Package { p := pkg.Package{ Name: d.Name, Version: d.Version, Language: pkg.Elixir, Locations: file.NewLocationSet(locations...), - PURL: packageURL(d), + PURL: packageURL(source, d), Type: pkg.HexPkg, Metadata: d, } @@ -22,7 +27,14 @@ func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Packag return p } -func packageURL(m pkg.ElixirMixLockEntry) string { +func packageURL(source string, m pkg.ElixirMixLockEntry) string { + // Non-hex sources (git, path) are not published to the hex.pm registry, so a + // pkg:hex/ PURL would be incorrect and would drive false hex.pm vulnerability + // matches. Emit no PURL for them. + if source != "hex" { + return "" + } + var qualifiers packageurl.Qualifiers return packageurl.NewPackageURL( diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock.go b/syft/pkg/cataloger/elixir/parse_mix_lock.go index b444cde2a..e44aaa8c0 100644 --- a/syft/pkg/cataloger/elixir/parse_mix_lock.go +++ b/syft/pkg/cataloger/elixir/parse_mix_lock.go @@ -53,7 +53,34 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re errs = unknown.Appendf(errs, reader, "unable to read mix lock line %d: %s", lineNum, line) continue } - name, version, hash, hashExt := tokens[1], tokens[4], tokens[5], tokens[len(tokens)-2] + + // tokens[2] is the source atom of the entry's tuple: `hex`, `git`, or + // `path`. The layout of the remaining tokens differs per source, so the + // version/hash positions must be read accordingly. Only hex entries are + // backed by the hex.pm registry; git/path entries must not be emitted as + // hex packages (see newPackage), otherwise a bogus pkg:hex/ PURL produces + // false hex.pm vulnerability matches. + source := tokens[2] + + var name, version, hash, hashExt string + switch source { + case "git": + // e.g. `"dep": {:git, "https://host/dep.git", "", [ref: "..."]}` + // tokens: ["", name, "git", "", "//host/dep.git", "", ...] + // The version is the commit SHA/ref immediately after the URL; there + // is no hex checksum for a git-sourced dependency. + name, version = tokens[1], tokens[5] + case "path": + // e.g. `"dep": {:path, "../local", []}` + // tokens: ["", name, "path", "../local", "[]", ""] + // A path dependency has no version or checksum; tokens[4] is the empty + // dependency list `[]`, not a version. + name = tokens[1] + default: + // hex (and any registry-style tuple): keep the original behavior. + // tokens: ["", name, "hex", name, version, hash, ..., hashExt, ""] + name, version, hash, hashExt = tokens[1], tokens[4], tokens[5], tokens[len(tokens)-2] + } if name == "" { log.WithFields("path", reader.RealPath).Debug("skipping empty package name from mix.lock file") @@ -63,6 +90,7 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re packages = append(packages, newPackage( + source, pkg.ElixirMixLockEntry{ Name: name, Version: version, diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go index 0e1e36e3c..c19e908d0 100644 --- a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go +++ b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go @@ -230,6 +230,34 @@ func TestParseMixLock(t *testing.T) { Dependencies: []string{"decimal"}, }, }, + { + // git-sourced dependency: version is the commit SHA (not a URL + // fragment) and no pkg:hex/ PURL is emitted, so it cannot be + // matched as a hex.pm package. + Name: "enacl", + Version: "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", + Language: pkg.Elixir, + Type: pkg.HexPkg, + Locations: locations, + PURL: "", + Metadata: pkg.ElixirMixLockEntry{ + Name: "enacl", + Version: "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", + }, + }, + { + // path-sourced dependency: no version (tokens[4] is the empty + // dependency list `[]`, not a version) and no pkg:hex/ PURL. + Name: "local_dep", + Version: "", + Language: pkg.Elixir, + Type: pkg.HexPkg, + Locations: locations, + PURL: "", + Metadata: pkg.ElixirMixLockEntry{ + Name: "local_dep", + }, + }, } fixture := "testdata/mix.lock" diff --git a/syft/pkg/cataloger/elixir/testdata/mix.lock b/syft/pkg/cataloger/elixir/testdata/mix.lock index 8d9806c1b..8b67d4e02 100644 --- a/syft/pkg/cataloger/elixir/testdata/mix.lock +++ b/syft/pkg/cataloger/elixir/testdata/mix.lock @@ -14,4 +14,6 @@ "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"}, "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "enacl": {:git, "https://github.com/aeternity/enacl.git", "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", [ref: "2f50ba6"]}, + "local_dep": {:path, "../local_dep", []}, } From ebac92a86ebae0002ec5bee50c2f5ebf6f32b26b Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 6 Jul 2026 15:17:42 +0000 Subject: [PATCH 051/168] fix: conanfile comments in [requires] no longer stop dependency detection (#5020) The [requires] section parser set inRequirements=false when it encountered a comment line (#) inside the section, because the comment check was OR'd with the section-change check. Any dependency listed after a comment was silently skipped. Skip blank lines and comments before evaluating section state so they never toggle inRequirements. Add a regression test fixture with a comment inside [requires]. Fixes #5017 Signed-off-by: jeff --- syft/pkg/cataloger/cpp/parse_conanfile.go | 17 ++++++---- .../pkg/cataloger/cpp/parse_conanfile_test.go | 33 +++++++++++++++++++ .../conanfile-comment-in-requires.txt | 9 +++++ 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt diff --git a/syft/pkg/cataloger/cpp/parse_conanfile.go b/syft/pkg/cataloger/cpp/parse_conanfile.go index 5ae124051..677a1b8cb 100644 --- a/syft/pkg/cataloger/cpp/parse_conanfile.go +++ b/syft/pkg/cataloger/cpp/parse_conanfile.go @@ -31,15 +31,20 @@ func parseConanfile(_ context.Context, _ file.Resolver, _ *generic.Environment, return nil, nil, fmt.Errorf("failed to parse conanfile.txt file: %w", err) } + trimmed := strings.TrimSpace(line) + + // skip blank lines and comments without affecting section state + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + switch { case strings.Contains(line, "[requires]"): inRequirements = true - case strings.ContainsAny(line, "[]") || strings.HasPrefix(strings.TrimSpace(line), "#"): + continue + case strings.ContainsAny(line, "[]"): inRequirements = false - } - - m := pkg.ConanfileEntry{ - Ref: strings.TrimSpace(line), + continue } if !inRequirements { @@ -47,7 +52,7 @@ func parseConanfile(_ context.Context, _ file.Resolver, _ *generic.Environment, } p := newConanfilePackage( - m, + pkg.ConanfileEntry{Ref: trimmed}, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ) if p == nil { diff --git a/syft/pkg/cataloger/cpp/parse_conanfile_test.go b/syft/pkg/cataloger/cpp/parse_conanfile_test.go index 53995493b..9fc6a9384 100644 --- a/syft/pkg/cataloger/cpp/parse_conanfile_test.go +++ b/syft/pkg/cataloger/cpp/parse_conanfile_test.go @@ -86,3 +86,36 @@ func TestParseConanfile(t *testing.T) { pkgtest.TestFileParser(t, fixture, parseConanfile, expected, expectedRelationships) } + +func TestParseConanfileCommentInRequires(t *testing.T) { + fixture := "testdata/conanfile-comment-in-requires.txt" + fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture)) + expected := []pkg.Package{ + { + Name: "catch2", + Version: "2.13.8", + PURL: "pkg:conan/catch2@2.13.8", + Locations: fixtureLocationSet, + Language: pkg.CPP, + Type: pkg.ConanPkg, + Metadata: pkg.ConanfileEntry{ + Ref: "catch2/2.13.8", + }, + }, + { + Name: "docopt.cpp", + Version: "0.6.3", + PURL: "pkg:conan/docopt.cpp@0.6.3", + Locations: fixtureLocationSet, + Language: pkg.CPP, + Type: pkg.ConanPkg, + Metadata: pkg.ConanfileEntry{ + Ref: "docopt.cpp/0.6.3", + }, + }, + } + + var expectedRelationships []artifact.Relationship + + pkgtest.TestFileParser(t, fixture, parseConanfile, expected, expectedRelationships) +} diff --git a/syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt b/syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt new file mode 100644 index 000000000..207158045 --- /dev/null +++ b/syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt @@ -0,0 +1,9 @@ +# Docs at https://docs.conan.io/en/latest/reference/conanfile_txt.html + +[requires] +# this is a comment inside requires +catch2/2.13.8 +docopt.cpp/0.6.3 + +[generators] +cmake_find_package_multi From 3e399256b3dc36b699d5e9cec33928f7e3c12c5e Mon Sep 17 00:00:00 2001 From: OsamaSE <25825931+OsamaSE@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:18:37 +0400 Subject: [PATCH 052/168] fix(debian): supplement point release from /etc/debian_version (#4997) Debian's /etc/os-release declares only the major version in VERSION_ID (e.g. "13"), while /etc/debian_version carries the point release (e.g. "13.5"). The cataloger previously skipped debian_version whenever os-release already had a Version/VersionID, so point releases were lost. Signed-off-by: Osama Elhindawy <25825931+OsamaSE@users.noreply.github.com> --- syft/linux/identify_release_test.go | 16 ++++++++++++++++ syft/linux/supplement_release.go | 9 +++++---- .../debian/with-point-release/etc/debian_version | 1 + .../os/debian/with-point-release/etc/os-release | 9 +++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 syft/linux/testdata/os/debian/with-point-release/etc/debian_version create mode 100644 syft/linux/testdata/os/debian/with-point-release/etc/os-release diff --git a/syft/linux/identify_release_test.go b/syft/linux/identify_release_test.go index d1f67f957..1c719351d 100644 --- a/syft/linux/identify_release_test.go +++ b/syft/linux/identify_release_test.go @@ -101,6 +101,22 @@ func TestIdentifyRelease(t *testing.T) { BugReportURL: "https://github.com/GoogleContainerTools/distroless/issues/new", }, }, + { + // standard Debian: VERSION_ID has major only, debian_version has the point release + fixture: "testdata/os/debian/with-point-release", + release: &Release{ + PrettyName: "Debian GNU/Linux 13 (trixie)", + Name: "Debian GNU/Linux", + ID: "debian", + IDLike: nil, + Version: "13 (trixie)", + VersionID: "13.5", + VersionCodename: "trixie", + HomeURL: "https://www.debian.org/", + SupportURL: "https://www.debian.org/support", + BugReportURL: "https://bugs.debian.org/", + }, + }, { fixture: "testdata/os/fedora", release: &Release{ diff --git a/syft/linux/supplement_release.go b/syft/linux/supplement_release.go index dfa2c9b28..33a6815f6 100644 --- a/syft/linux/supplement_release.go +++ b/syft/linux/supplement_release.go @@ -11,11 +11,10 @@ import ( ) func supplementDebianVersion(resolver file.Resolver, release *Release) { - // we're only looking for version information for debian when none is present in /etc/os-release - if release.Version != "" || release.VersionID != "" || !strings.EqualFold(release.ID, "debian") { + if !strings.EqualFold(release.ID, "debian") { return } - // if we have a debian release with no version, look for a debian_version + // debian_version has a more specific point release than os-release locations, err := resolver.FilesByGlob("/etc/debian_version") if err != nil { log.Debugf("error reading /etc/debian_version: %v", err) @@ -24,8 +23,10 @@ func supplementDebianVersion(resolver file.Resolver, release *Release) { for _, location := range locations { version := readDebianVersionFile(resolver, location) if version != "" { - release.Version = version release.VersionID = version + if release.Version == "" { + release.Version = version + } return // keep the first result } } diff --git a/syft/linux/testdata/os/debian/with-point-release/etc/debian_version b/syft/linux/testdata/os/debian/with-point-release/etc/debian_version new file mode 100644 index 000000000..e09420105 --- /dev/null +++ b/syft/linux/testdata/os/debian/with-point-release/etc/debian_version @@ -0,0 +1 @@ +13.5 diff --git a/syft/linux/testdata/os/debian/with-point-release/etc/os-release b/syft/linux/testdata/os/debian/with-point-release/etc/os-release new file mode 100644 index 000000000..e99969d90 --- /dev/null +++ b/syft/linux/testdata/os/debian/with-point-release/etc/os-release @@ -0,0 +1,9 @@ +PRETTY_NAME="Debian GNU/Linux 13 (trixie)" +NAME="Debian GNU/Linux" +VERSION_ID="13" +VERSION="13 (trixie)" +VERSION_CODENAME=trixie +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" From 002a326c41e9fc14def8869bfafa3614b5016b4b Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 7 Jul 2026 10:11:15 -0400 Subject: [PATCH 053/168] detect Ubuntu Pro/ESM extended support (#5028) Mirrors the existing RHEL `ExtendedSupport` detector for Ubuntu so grype can auto-enable its ESM distro channel on Pro/ESM-enabled scans. `findUbuntuFeatures` sets `linux.Release.ExtendedSupport` when the scanned OS is Ubuntu and any of these hold: - an uncommented apt source (classic `.list` or DEB822 `.sources`, or the ubuntu-advantage auth entry) referencing `esm.ubuntu.com` - an enabled `esm-infra`/`esm-apps` service in the ubuntu-advantage `status.json` cache - an installed dpkg package whose version carries an `[~+]esm` pocket suffix (the usual in-image signal, since containers are rarely Pro-attached) The package fallback reads `s.Artifacts.Packages` from the `DetectFeatures` SBOM callback, which runs after package cataloging, so no new plumbing was needed. Conservative by design: plain Ubuntu and disabled/commented ESM evidence stay false. Signed-off-by: Alex Goodman --- internal/os/feature_detection.go | 170 +++++++++++++- internal/os/feature_detection_test.go | 214 ++++++++++++++++++ .../apt/sources.list.d/ubuntu-esm-infra.list | 6 + .../etc/apt/auth.conf.d/90ubuntu-advantage | 2 + .../sources.list.d/ubuntu-esm-infra.sources | 5 + .../apt/sources.list.d/ubuntu-esm-infra.list | 3 + .../var/lib/ubuntu-advantage/status.json | 18 ++ .../var/lib/ubuntu-advantage/status.json | 21 ++ .../var/lib/ubuntu-advantage/status.json | 1 + .../var/lib/ubuntu-advantage/status.json | 28 +++ .../etc/apt/sources.list.d/ubuntu-fips.list | 3 + .../var/lib/ubuntu-advantage/status.json | 28 +++ .../etc/apt/sources.list.d/ubuntu.sources | 5 + 13 files changed, 493 insertions(+), 11 deletions(-) create mode 100644 internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list create mode 100644 internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage create mode 100644 internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources create mode 100644 internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list create mode 100644 internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json create mode 100644 internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json create mode 100644 internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json create mode 100644 internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json create mode 100644 internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list create mode 100644 internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json create mode 100644 internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources diff --git a/internal/os/feature_detection.go b/internal/os/feature_detection.go index c23b8b67b..1aaf3f23c 100644 --- a/internal/os/feature_detection.go +++ b/internal/os/feature_detection.go @@ -2,6 +2,8 @@ package os import ( "context" + "encoding/json" + "errors" "fmt" "regexp" @@ -10,6 +12,7 @@ import ( "github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) @@ -19,8 +22,11 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync. return } - err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution) - if err != nil { + if err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution); err != nil { + log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support") + } + + if err := findUbuntuFeatures(resolver, s.Artifacts.LinuxDistribution, s.Artifacts.Packages); err != nil { log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support") } }) @@ -28,6 +34,9 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync. return nil } +// rhelEUSPatterns match a RHEL content manifest referencing an extended-update-support (EUS) repo. +var rhelEUSPatterns = []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)} + func findRhelFeatures(resolver file.Resolver, release *linux.Release) error { if release == nil || release.ID != "rhel" { return nil @@ -37,9 +46,9 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error { return fmt.Errorf("unable to find content manifests: %w", err) } for _, contentManifestFile := range contentManifestFiles { - found, err := hasRhelExtendedSupportInContentManifest(resolver, contentManifestFile) + found, err := fileMatchesAny(resolver, contentManifestFile, rhelEUSPatterns...) if err != nil { - return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err) + return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.RealPath, err) } if found { release.ExtendedSupport = true @@ -49,13 +58,152 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error { return nil } -func hasRhelExtendedSupportInContentManifest(resolver file.Resolver, contentManifestFile file.Location) (bool, error) { - contents, err := resolver.FileContentsByLocation(contentManifestFile) - if err != nil { - return false, fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err) - } - defer internal.CloseAndLogError(contents, "content-manifest") +// Ubuntu Pro (formerly Ubuntu Advantage) is Canonical's subscription that unlocks Expanded Security +// Maintenance (ESM): security fixes past a package's standard support window. we detect the two base ESM +// streams, esm-infra (the `main` repo) and esm-apps (`universe`), and collapse them into one ExtendedSupport +// signal; the infra-vs-apps split is disambiguated by the downstream consumer, not by this boolean. +// +// the other Pro streams on the same host (esm.ubuntu.com/fips, /fips-updates, /realtime) are deliberately +// excluded: they are separate compliance products with their own downstream channels, and a host can run +// them with base ESM disabled, so folding them in would be unsound. +// +// detection is deliberately forgiving: apt/status evidence disappears when ESM is disabled, but an installed +// +esmN package is durable proof ESM content is on disk. so ExtendedSupport means "ESM was or is in effect +// for this host or its content", not "currently entitled" (mere eligibility, without evidence, is not enough). - patterns := []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)} +// esmAptSourcePattern matches an uncommented apt source (classic or DEB822) or auth entry pointing at the +// plain ESM streams esm.ubuntu.com/infra or /apps. it deliberately scopes to /(infra|apps) so the other +// Pro streams served from the same host (/fips, /fips-updates, /realtime) are never folded into the base +// esm channel. the [^#\n]* before the host ensures a leading `#` (comment) can never satisfy the match. +var esmAptSourcePattern = regexp.MustCompile(`(?m)^[^#\n]*esm\.ubuntu\.com/(infra|apps)`) + +// esmVersionPattern matches a dpkg version carrying a numbered ESM pocket suffix, e.g. `...+esm1` or +// `...~esm2`. the trailing \d scopes this to the ESM pocket and excludes other Pro pockets (e.g. +fips). +var esmVersionPattern = regexp.MustCompile(`[~+]esm\d`) + +func findUbuntuFeatures(resolver file.Resolver, release *linux.Release, packages *pkg.Collection) error { + if release == nil || release.ID != "ubuntu" { + return nil + } + + // an uncommented esm.ubuntu.com apt source or an active pro esm service both prove Pro/ESM was enabled. + // these are the durable, file-based signals present on attached hosts. a read error in one signal must + // not suppress the others, so errors are collected and detection continues to the next signal. + var errs error + for _, check := range []func() (bool, error){ + func() (bool, error) { return hasUbuntuESMAptEvidence(resolver) }, + func() (bool, error) { return hasActiveUbuntuESMService(resolver) }, + } { + found, err := check() + if err != nil { + errs = errors.Join(errs, err) + continue + } + if found { + release.ExtendedSupport = true + return errs + } + } + + // fallback for images (typically not pro-attached): an installed package whose version carries an + // +esm/~esm suffix was pulled from an ESM pocket, so ESM was in effect when it was installed. + if hasInstalledESMPackage(packages) { + release.ExtendedSupport = true + } + return errs +} + +// hasUbuntuESMAptEvidence reports whether any apt source (classic .list or DEB822 .sources) or auth entry +// references esm.ubuntu.com via an uncommented line. +func hasUbuntuESMAptEvidence(resolver file.Resolver) (bool, error) { + // apt source files live in a directory (classic .list or DEB822 .sources), the credentials file is a fixed path. + sourceLocations, err := resolver.FilesByGlob("/etc/apt/sources.list.d/*") + if err != nil { + return false, fmt.Errorf("unable to find apt esm sources: %w", err) + } + authLocations, err := resolver.FilesByPath("/etc/apt/auth.conf.d/90ubuntu-advantage") + if err != nil { + return false, fmt.Errorf("unable to find apt esm auth: %w", err) + } + + // ponytail: a DEB822 source with `Enabled: no` would still match; the pro client removes/comments the + // file on disable rather than toggling that key, so this is an accepted false-positive ceiling. the + // auth.conf.d entry is deleted by the pro client on disable, so it is a durable positive-only signal. + for _, location := range append(sourceLocations, authLocations...) { + match, err := fileMatchesAny(resolver, location, esmAptSourcePattern) + if err != nil { + return false, err + } + if match { + return true, nil + } + } + return false, nil +} + +// hasActiveUbuntuESMService reports whether the ubuntu-advantage/pro status cache shows an enabled esm service. +func hasActiveUbuntuESMService(resolver file.Resolver) (bool, error) { + locations, err := resolver.FilesByPath("/var/lib/ubuntu-advantage/status.json") + if err != nil { + return false, fmt.Errorf("unable to find ubuntu-advantage status: %w", err) + } + for _, location := range locations { + enabled, err := hasEnabledESMServiceInStatus(resolver, location) + if err != nil { + return false, err + } + if enabled { + return true, nil + } + } + return false, nil +} + +func hasEnabledESMServiceInStatus(resolver file.Resolver, location file.Location) (bool, error) { + contents, err := resolver.FileContentsByLocation(location) + if err != nil { + return false, fmt.Errorf("unable to read ubuntu-advantage status from %s: %w", location.RealPath, err) + } + defer internal.CloseAndLogError(contents, location.RealPath) + + var status struct { + Services []struct { + Name string `json:"name"` + Status string `json:"status"` + } `json:"services"` + } + if err := json.NewDecoder(contents).Decode(&status); err != nil { + return false, fmt.Errorf("unable to parse ubuntu-advantage status from %s: %w", location.RealPath, err) + } + + // pro status values are enabled/disabled/n-a/warning/—; only "enabled" proves the stream is active. + // warning/expired states are intentionally excluded here and left to the apt-source and installed-package signals. + for _, svc := range status.Services { + if (svc.Name == "esm-infra" || svc.Name == "esm-apps") && svc.Status == "enabled" { + return true, nil + } + } + return false, nil +} + +// hasInstalledESMPackage reports whether any installed dpkg package version carries an ESM pocket suffix. +func hasInstalledESMPackage(packages *pkg.Collection) bool { + if packages == nil { + return false + } + for p := range packages.Enumerate(pkg.DebPkg) { + if esmVersionPattern.MatchString(p.Version) { + return true + } + } + return false +} + +func fileMatchesAny(resolver file.Resolver, location file.Location, patterns ...*regexp.Regexp) (bool, error) { + contents, err := resolver.FileContentsByLocation(location) + if err != nil { + return false, fmt.Errorf("unable to read %s: %w", location.RealPath, err) + } + defer internal.CloseAndLogError(contents, location.RealPath) return internal.MatchAnyFromReader(contents, patterns...) } diff --git a/internal/os/feature_detection_test.go b/internal/os/feature_detection_test.go index 9a1fe32f9..bb8a30de1 100644 --- a/internal/os/feature_detection_test.go +++ b/internal/os/feature_detection_test.go @@ -2,6 +2,11 @@ package os_test import ( "context" + "io" + stdos "os" + "path" + "path/filepath" + "slices" "testing" "github.com/stretchr/testify/require" @@ -11,7 +16,9 @@ import ( "github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/internal/task" "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -99,3 +106,210 @@ func Test_EnvironmentTask(t *testing.T) { }) } } + +func Test_findUbuntuFeatures(t *testing.T) { + tests := []struct { + name string + dir string + releaseID string + packages []pkg.Package + expected bool + }{ + { + name: "esm apt source present", + dir: "testdata/ubuntu_esm_apt", + releaseID: "ubuntu", + expected: true, + }, + { + name: "esm apt source in DEB822 .sources format", + dir: "testdata/ubuntu_esm_deb822", + releaseID: "ubuntu", + expected: true, + }, + { + name: "esm evidence in apt auth.conf.d", + dir: "testdata/ubuntu_esm_authconf", + releaseID: "ubuntu", + expected: true, + }, + { + name: "active esm service in ubuntu-advantage status", + dir: "testdata/ubuntu_esm_status", + releaseID: "ubuntu", + expected: true, + }, + { + name: "only esm-infra enabled (esm-apps disabled)", + dir: "testdata/ubuntu_esm_infra_only", + releaseID: "ubuntu", + expected: true, + }, + { + name: "malformed status.json does not panic or match", + dir: "testdata/ubuntu_esm_malformed", + releaseID: "ubuntu", + expected: false, + }, + { + name: "installed +esm package version", + dir: "testdata/ubuntu_plain", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg}, + }, + expected: true, + }, + { + name: "installed ~esm package version", + dir: "testdata/ubuntu_plain", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "libcap2", Version: "1:2.32-1ubuntu0.1~esm1", Type: pkg.DebPkg}, + }, + expected: true, + }, + { + name: "plain ubuntu with no esm evidence", + dir: "testdata/ubuntu_plain", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19", Type: pkg.DebPkg}, + }, + expected: false, + }, + { + name: "commented esm source and disabled esm service", + dir: "testdata/ubuntu_esm_disabled", + releaseID: "ubuntu", + expected: false, + }, + { + name: "esm disabled but +esm package remains installed", + dir: "testdata/ubuntu_esm_disabled", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg}, + }, + expected: true, // durable proof: ESM content is on disk even though the channel is now off + }, + { + name: "fips-only pro host is not plain esm", + dir: "testdata/ubuntu_fips_only", + releaseID: "ubuntu", + expected: false, // esm.ubuntu.com/fips must not be folded into the base esm channel + }, + { + name: "non-ubuntu is unaffected by esm evidence", + dir: "testdata/ubuntu_esm_apt", + releaseID: "debian", + expected: false, + }, + { + name: "non-ubuntu is unaffected by esm package version", + dir: "testdata/ubuntu_plain", + releaseID: "debian", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg}, + }, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resolver := fixtureResolverForDir(t, test.dir) + + s := sbom.SBOM{} + s.Artifacts.LinuxDistribution = &linux.Release{ID: test.releaseID} + s.Artifacts.Packages = pkg.NewCollection(test.packages...) + + err := os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s)) + require.NoError(t, err) + + require.Equal(t, test.expected, s.Artifacts.LinuxDistribution.ExtendedSupport) + }) + } +} + +// a read error in the apt-source signal must not suppress the ubuntu-advantage status signal; the three +// signals are meant to be independent fallbacks. +func Test_findUbuntuFeatures_signalErrorDoesNotSuppressOthers(t *testing.T) { + resolver := errOnGlobResolver{ + fixtureResolver: fixtureResolverForDir(t, "testdata/ubuntu_esm_status").(fixtureResolver), + errOnGlob: "/etc/apt/sources.list.d/*", + } + + s := sbom.SBOM{} + s.Artifacts.LinuxDistribution = &linux.Release{ID: "ubuntu"} + s.Artifacts.Packages = pkg.NewCollection() + + require.NoError(t, os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s))) + + // the apt glob errors, but the status.json signal (esm-infra enabled) should still be consulted + require.True(t, s.Artifacts.LinuxDistribution.ExtendedSupport) +} + +// errOnGlobResolver injects a failure for a specific glob to simulate an unreadable apt directory. +type errOnGlobResolver struct { + fixtureResolver + errOnGlob string +} + +func (r errOnGlobResolver) FilesByGlob(patterns ...string) ([]file.Location, error) { + if slices.Contains(patterns, r.errOnGlob) { + return nil, stdos.ErrPermission + } + return r.fixtureResolver.FilesByGlob(patterns...) +} + +// fixtureResolver maps in-image logical paths to on-disk fixture files so DetectFeatures can be driven +// without building a container image. +type fixtureResolver struct { + file.Resolver // unused methods; nil is fine since only the three below are called + files map[string]string +} + +func (r fixtureResolver) FilesByGlob(patterns ...string) ([]file.Location, error) { + var out []file.Location + for logical := range r.files { + for _, p := range patterns { + if ok, _ := path.Match(p, logical); ok { + out = append(out, file.NewLocation(logical)) + break + } + } + } + return out, nil +} + +func (r fixtureResolver) FilesByPath(paths ...string) ([]file.Location, error) { + var out []file.Location + for _, p := range paths { + if _, ok := r.files[p]; ok { + out = append(out, file.NewLocation(p)) + } + } + return out, nil +} + +func (r fixtureResolver) FileContentsByLocation(l file.Location) (io.ReadCloser, error) { + return stdos.Open(r.files[l.RealPath]) +} + +func fixtureResolverForDir(t *testing.T, dir string) file.Resolver { + t.Helper() + files := map[string]string{} + require.NoError(t, filepath.WalkDir(dir, func(p string, d stdos.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + rel, err := filepath.Rel(dir, p) + if err != nil { + return err + } + files["/"+filepath.ToSlash(rel)] = p + return nil + })) + return fixtureResolver{files: files} +} diff --git a/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list b/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list new file mode 100644 index 000000000..8f43ca9a2 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list @@ -0,0 +1,6 @@ +# Written by ubuntu-advantage-tools +deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main +# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main + +deb https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main +# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main diff --git a/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage b/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage new file mode 100644 index 000000000..74c7c3018 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage @@ -0,0 +1,2 @@ +machine esm.ubuntu.com/infra/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client +machine esm.ubuntu.com/apps/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client diff --git a/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources b/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources new file mode 100644 index 000000000..12df00a6e --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources @@ -0,0 +1,5 @@ +Types: deb +URIs: https://esm.ubuntu.com/infra/ubuntu +Suites: noble-infra-security noble-infra-updates +Components: main +Signed-By: /usr/share/keyrings/ubuntu-pro-esm-infra-keyring.gpg diff --git a/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list b/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list new file mode 100644 index 000000000..7351dd71c --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list @@ -0,0 +1,3 @@ +# Written by ubuntu-advantage-tools +# deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main +# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main diff --git a/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..3cd190426 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,18 @@ +{ + "_schema_version": "0.1", + "attached": true, + "services": [ + { + "name": "esm-apps", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "esm-infra", + "available": "yes", + "entitled": "yes", + "status": "disabled" + } + ] +} diff --git a/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..98407f493 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,21 @@ +{ + "_schema_version": "0.1", + "attached": true, + "execution_status": "success", + "services": [ + { + "name": "esm-apps", + "description": "Expanded Security Maintenance for Applications", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "esm-infra", + "description": "Expanded Security Maintenance for Infrastructure", + "available": "yes", + "entitled": "yes", + "status": "enabled" + } + ] +} diff --git a/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..dcf314808 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json @@ -0,0 +1 @@ +{ this is not valid json diff --git a/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..597f97c09 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,28 @@ +{ + "_doc": "Content provided in json response is currently considered Experimental and may change", + "_schema_version": "0.1", + "attached": true, + "services": [ + { + "name": "esm-apps", + "description": "Expanded Security Maintenance for Applications", + "available": "yes", + "entitled": "yes", + "status": "enabled" + }, + { + "name": "esm-infra", + "description": "Expanded Security Maintenance for Infrastructure", + "available": "yes", + "entitled": "yes", + "status": "enabled" + }, + { + "name": "fips", + "description": "NIST-certified FIPS crypto packages", + "available": "no", + "entitled": "no", + "status": "n/a" + } + ] +} diff --git a/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list b/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list new file mode 100644 index 000000000..14c22bda4 --- /dev/null +++ b/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list @@ -0,0 +1,3 @@ +# Written by ubuntu-pro-client +deb https://esm.ubuntu.com/fips/ubuntu focal main +# deb-src https://esm.ubuntu.com/fips/ubuntu focal main diff --git a/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..6926f3151 --- /dev/null +++ b/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,28 @@ +{ + "_schema_version": "0.1", + "attached": true, + "execution_status": "success", + "services": [ + { + "name": "esm-apps", + "description": "Expanded Security Maintenance for Applications", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "esm-infra", + "description": "Expanded Security Maintenance for Infrastructure", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "fips", + "description": "NIST-certified FIPS crypto packages", + "available": "yes", + "entitled": "yes", + "status": "enabled" + } + ] +} diff --git a/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources b/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources new file mode 100644 index 000000000..7552b963f --- /dev/null +++ b/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources @@ -0,0 +1,5 @@ +Types: deb +URIs: http://archive.ubuntu.com/ubuntu/ +Suites: noble noble-updates noble-backports +Components: main restricted universe multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg From e8abab2e775ffa844f4ce978c606769d23bdf703 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Wed, 8 Jul 2026 09:55:13 -0700 Subject: [PATCH 054/168] swift: emit canonical purls without .git suffix or repeated name (#4785) * swift: emit canonical purls without .git suffix or repeated name Reported in anchore/syft#3961: the purl syft emits for Swift packages parsed out of Package.resolved keeps the repository URLs .git suffix and then re-appends the package name, producing pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0 NVD / Grype cannot match that purl against the known swift-nio-ssl@2.0.0 CVE (GHSA-frg3-gpcx-968f), so every Swift SBOM produced by syft silently loses vulnerability coverage. cdxgen and the wider purl ecosystem use the shorter form pkg:swift/github.com/apple/swift-nio-ssl@2.0.0 which Grype does match. Replace the ad-hoc strings.Replace with swiftNamespaceFromSourceURL, which trims the common URL schemes, strips the ".git" suffix, and drops a trailing / segment so the namespace is only the organisation path (e.g. github.com/apple). Existing test expectations are updated to the new purl shape. Fixes #3961 Signed-off-by: Sai Asish Y Signed-off-by: Alex Goodman * Revert unrelated consul binary classifier changes The swift purl commit (ff5ffc37) accidentally bundled removal of the consul GitDescribe and NUL-wrapped version matchers plus the 1.12.9 and 1.7.14 fixtures/test cases. That drops version detection for consul binaries not carrying the CONSUL_VERSION string. Restore the binary cataloger to its pre-commit state so this branch is swift-only. Signed-off-by: Alex Goodman * swift: strip repeated name segment case-insensitively Package.resolved identities are lowercased while the repo URL path may be mixed-case (e.g. github.com/Apple/Swift-NIO vs identity swift-nio). The case-sensitive TrimSuffix left the repo segment in the namespace for such repos, reintroducing the duplicated-name purl. Compare the trailing segment with EqualFold and add unit coverage for swiftPackageManagerPackageURL. Signed-off-by: Alex Goodman --------- Signed-off-by: Sai Asish Y Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/swift/package.go | 36 ++++++++++++- syft/pkg/cataloger/swift/package_test.go | 51 +++++++++++++++++++ .../swift/parse_package_resolved_test.go | 14 ++--- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/syft/pkg/cataloger/swift/package.go b/syft/pkg/cataloger/swift/package.go index c4370f5dc..17870370a 100644 --- a/syft/pkg/cataloger/swift/package.go +++ b/syft/pkg/cataloger/swift/package.go @@ -62,10 +62,44 @@ func swiftPackageManagerPackageURL(name, version, sourceURL string) string { return packageurl.NewPackageURL( packageurl.TypeSwift, - strings.Replace(sourceURL, "https://", "", 1), + swiftNamespaceFromSourceURL(sourceURL, name), name, version, qualifiers, "", ).ToString() } + +// swiftNamespaceFromSourceURL derives the purl namespace (e.g. +// "github.com/apple") from a Swift Package.resolved source URL like +// "https://github.com/apple/swift-nio-ssl.git". +// +// Two bits of cleanup are needed compared with the previous +// strings.Replace(sourceURL, "https://", "", 1) behaviour: +// +// 1. Strip the ".git" suffix. Swift Package.resolved always carries the +// repository URL with a trailing ".git", but the purl spec uses the +// plain web path. +// 2. Drop the trailing / segment. The previous code left the repo +// name in the namespace and then appended the package name again, so +// the emitted purl looked like +// pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0 +// which Grype cannot match against NVD. The expected form is +// pkg:swift/github.com/apple/swift-nio-ssl@2.0.0 +// which is also what cdxgen emits. See anchore/syft#3961. +func swiftNamespaceFromSourceURL(sourceURL, name string) string { + ns := sourceURL + for _, prefix := range []string{"https://", "http://", "git+ssh://", "ssh://"} { + ns = strings.TrimPrefix(ns, prefix) + } + ns = strings.TrimSuffix(ns, ".git") + // drop a trailing "/" segment. Package.resolved identities are lowercased + // while the repo path may be mixed-case (e.g. github.com/Apple/Swift-NIO vs + // identity swift-nio), so compare case-insensitively. + if name != "" { + if idx := strings.LastIndex(ns, "/"); idx >= 0 && strings.EqualFold(ns[idx+1:], name) { + ns = ns[:idx] + } + } + return ns +} diff --git a/syft/pkg/cataloger/swift/package_test.go b/syft/pkg/cataloger/swift/package_test.go index daa3132cd..abc8f3145 100644 --- a/syft/pkg/cataloger/swift/package_test.go +++ b/syft/pkg/cataloger/swift/package_test.go @@ -6,6 +6,57 @@ import ( "github.com/stretchr/testify/assert" ) +func Test_swiftPackageManagerPackageURL(t *testing.T) { + tests := []struct { + name string + pkgName string + version string + sourceURL string + want string + }{ + { + name: "strips .git suffix and repeated name segment", + pkgName: "swift-nio-ssl", + version: "2.0.0", + sourceURL: "https://github.com/apple/swift-nio-ssl.git", + want: "pkg:swift/github.com/apple/swift-nio-ssl@2.0.0", + }, + { + name: "no .git suffix", + pkgName: "swift-numerics", + version: "1.0.2", + sourceURL: "https://github.com/apple/swift-numerics", + want: "pkg:swift/github.com/apple/swift-numerics@1.0.2", + }, + { + name: "mixed-case repo path with lowercased identity", + pkgName: "swift-nio", + version: "2.0.0", + sourceURL: "https://github.com/Apple/Swift-NIO.git", + want: "pkg:swift/github.com/Apple/swift-nio@2.0.0", + }, + { + name: "ssh scheme", + pkgName: "swift-nio", + version: "2.0.0", + sourceURL: "git+ssh://github.com/apple/swift-nio.git", + want: "pkg:swift/github.com/apple/swift-nio@2.0.0", + }, + { + name: "trailing segment differs from name is preserved", + pkgName: "nio", + version: "2.0.0", + sourceURL: "https://github.com/apple/swift-nio.git", + want: "pkg:swift/github.com/apple/swift-nio/nio@2.0.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, swiftPackageManagerPackageURL(tt.pkgName, tt.version, tt.sourceURL)) + }) + } +} + func Test_cocoaPodsPackageURL(t *testing.T) { type args struct { name string diff --git a/syft/pkg/cataloger/swift/parse_package_resolved_test.go b/syft/pkg/cataloger/swift/parse_package_resolved_test.go index cd2565924..494a164c1 100644 --- a/syft/pkg/cataloger/swift/parse_package_resolved_test.go +++ b/syft/pkg/cataloger/swift/parse_package_resolved_test.go @@ -20,7 +20,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-algorithms", Version: "1.0.0", - PURL: "pkg:swift/github.com/apple/swift-algorithms.git/swift-algorithms@1.0.0", + PURL: "pkg:swift/github.com/apple/swift-algorithms@1.0.0", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -31,7 +31,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-async-algorithms", Version: "0.1.0", - PURL: "pkg:swift/github.com/apple/swift-async-algorithms.git/swift-async-algorithms@0.1.0", + PURL: "pkg:swift/github.com/apple/swift-async-algorithms@0.1.0", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -42,7 +42,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-atomics", Version: "1.1.0", - PURL: "pkg:swift/github.com/apple/swift-atomics.git/swift-atomics@1.1.0", + PURL: "pkg:swift/github.com/apple/swift-atomics@1.1.0", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -53,7 +53,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-collections", Version: "1.0.4", - PURL: "pkg:swift/github.com/apple/swift-collections.git/swift-collections@1.0.4", + PURL: "pkg:swift/github.com/apple/swift-collections@1.0.4", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -64,7 +64,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-numerics", Version: "1.0.2", - PURL: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2", + PURL: "pkg:swift/github.com/apple/swift-numerics@1.0.2", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -87,7 +87,7 @@ func TestParsePackageResolvedV3(t *testing.T) { { Name: "swift-mmio", Version: "", - PURL: "pkg:swift/github.com/apple/swift-mmio/swift-mmio", + PURL: "pkg:swift/github.com/apple/swift-mmio", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -98,7 +98,7 @@ func TestParsePackageResolvedV3(t *testing.T) { { Name: "swift-syntax", Version: "509.1.1", - PURL: "pkg:swift/github.com/apple/swift-syntax.git/swift-syntax@509.1.1", + PURL: "pkg:swift/github.com/apple/swift-syntax@509.1.1", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, From 5b630ec61c7a3e98c51832fbab5fda79529b12a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:29:34 +0000 Subject: [PATCH 055/168] chore(deps): bump docker/login-action from 4.2.0 to 4.4.0 (#5049) Bumps [docker/login-action](https://github.com/docker/login-action) from 4.2.0 to 4.4.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...af1e73f918a031802d376d3c8bbc3fe56130a9b0) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 90776175a..9d3981475 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -79,13 +79,13 @@ jobs: uses: ./.github/actions/bootstrap - name: Login to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee #v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0 with: username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }} password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }} - name: Login to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee #v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} From c4a0b6f6470e628d5ea235029ab40d12f938bcd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:31:42 +0000 Subject: [PATCH 056/168] chore(deps): bump github.com/klauspost/compress from 1.18.6 to 1.19.0 (#5050) Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.6 to 1.19.0. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.6...v1.19.0) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5fc0388f1..dc3109696 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.8.1 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 - github.com/klauspost/compress v1.18.6 + github.com/klauspost/compress v1.19.0 github.com/magiconair/properties v1.8.10 github.com/mholt/archives v0.1.5 github.com/moby/sys/mountinfo v0.7.2 diff --git a/go.sum b/go.sum index e902f2d3c..bb20bd808 100644 --- a/go.sum +++ b/go.sum @@ -645,8 +645,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= From 3252b6f8063e5a2268eb3a2eb9dc42c271c1ab29 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 13 Jul 2026 10:07:00 -0400 Subject: [PATCH 057/168] chore(deps): update CPE dictionary index (#5063) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 1d7a1dd69..694bf48a5 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -5875,6 +5875,9 @@ "three": [ "cpe:2.3:a:three_project:three:*:*:*:*:*:node.js:*:*" ], + "thumbler": [ + "cpe:2.3:a:mmahrous:thumbler:*:*:*:*:*:node.js:*:*" + ], "tianma-static": [ "cpe:2.3:a:tianma-static_project:tianma-static:*:*:*:*:*:node.js:*:*" ], @@ -6961,7 +6964,7 @@ "cpe:2.3:a:google:slashify:*:*:*:*:*:node.js:*:*" ], "snowflake-connector-python": [ - "cpe:2.3:a:snowflake:snowflake-connector-python:*:*:*:*:*:*:*:*" + "cpe:2.3:a:snowflake:snowflake_connector:*:*:*:*:*:python:*:*" ], "sockeye": [ "cpe:2.3:a:amazon:sockeye:*:*:*:*:*:python:*:*" @@ -7041,6 +7044,9 @@ "xmpp-http-upload": [ "cpe:2.3:a:xmpp-http-upload_project:xmpp-http-upload:*:*:*:*:*:*:*:*" ], + "zeep": [ + "cpe:2.3:a:python-zeep:zeep:*:*:*:*:*:python:*:*" + ], "zibalPlatform": [ "cpe:2.3:a:zibal_project:zibal:*:*:*:*:*:pypi:*:*" ] @@ -10665,6 +10671,9 @@ "cpe:2.3:a:really-simple-plugins:complianz:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:really-simple-plugins:complianz:*:*:*:*:premium:wordpress:*:*" ], + "compress-then-upload": [ + "cpe:2.3:a:eliehanna:compress_\\\u0026_upload:*:*:*:*:*:wordpress:*:*" + ], "conditional-extra-fees-for-woocommerce": [ "cpe:2.3:a:piwebsolution:conditional_cart_fee_\\/_extra_charge_rule_for_woocommerce_extra_fees:*:*:*:*:*:wordpress:*:*" ], @@ -11408,6 +11417,9 @@ "database-peek": [ "cpe:2.3:a:database_peek_project:database_peek:*:*:*:*:*:wordpress:*:*" ], + "db-access": [ + "cpe:2.3:a:jimbob1953:db-access:*:*:*:*:*:wordpress:*:*" + ], "dc-woocommerce-multi-vendor": [ "cpe:2.3:a:multivendorx:multivendorx:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:wc-marketplace:multivendor_marketplace_solution_for_woocommerce_-_wc_marketplace:*:*:*:*:*:wordpress:*:*" @@ -18282,6 +18294,9 @@ "simple-side-tab": [ "cpe:2.3:a:rumspeed:simple_side_tab:*:*:*:*:*:wordpress:*:*" ], + "simple-signup-form": [ + "cpe:2.3:a:modalsurvey:simple_signup_form:*:*:*:*:*:wordpress:*:*" + ], "simple-site-verify": [ "cpe:2.3:a:idoweb:simple_site_verify:*:*:*:*:*:wordpress:*:*" ], @@ -22457,6 +22472,9 @@ "wpextended": [ "cpe:2.3:a:wpextended:wp_extended:*:*:*:*:*:wordpress:*:*" ], + "wpfavicon": [ + "cpe:2.3:a:nihal:wpfavicon:*:*:*:*:*:wordpress:*:*" + ], "wpforms": [ "cpe:2.3:a:wpforms:wpforms:*:*:*:*:pro:wordpress:*:*" ], From 1dcac54b0e10b7f61685be32707fde1695e5553e Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:30:23 -0400 Subject: [PATCH 058/168] feat: optionally capture golang binary symbols (#4988) --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- cmd/syft/internal/options/catalog.go | 3 +- cmd/syft/internal/options/golang.go | 13 +- cmd/syft/internal/options/golang_test.go | 57 + internal/capabilities/appconfig.yaml | 2 + internal/constants.go | 3 +- schema/json/schema-16.1.9.json | 4520 +++++++++++++++++ schema/json/schema-latest.json | 39 +- syft/cataloging/symbols.go | 28 + syft/cataloging/symbols_test.go | 30 + syft/pkg/cataloger/golang/capabilities.yaml | 3 + syft/pkg/cataloger/golang/cataloger.go | 5 +- syft/pkg/cataloger/golang/config.go | 12 + syft/pkg/cataloger/golang/config_test.go | 3 + syft/pkg/cataloger/golang/package.go | 3 +- syft/pkg/cataloger/golang/parse_go_binary.go | 51 +- .../cataloger/golang/parse_go_binary_test.go | 69 + syft/pkg/cataloger/golang/scan_binary.go | 16 +- syft/pkg/cataloger/golang/stdlib_package.go | 17 +- .../cataloger/golang/stdlib_package_test.go | 6 +- syft/pkg/cataloger/golang/symbols.go | 332 ++ syft/pkg/cataloger/golang/symbols_test.go | 201 + syft/pkg/golang.go | 7 + syft/pkg/vcpkg.go | 39 +- 23 files changed, 5414 insertions(+), 45 deletions(-) create mode 100644 cmd/syft/internal/options/golang_test.go create mode 100644 schema/json/schema-16.1.9.json create mode 100644 syft/cataloging/symbols.go create mode 100644 syft/cataloging/symbols_test.go create mode 100644 syft/pkg/cataloger/golang/symbols.go create mode 100644 syft/pkg/cataloger/golang/symbols_test.go diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 4ffbfad56..0d7e94b50 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -198,7 +198,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings). WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags), ). - WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)), + WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)). + WithCaptureSymbols(cfg.Golang.CaptureSymbols), JavaScript: javascript.DefaultCatalogerConfig(). WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)). WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)). diff --git a/cmd/syft/internal/options/golang.go b/cmd/syft/internal/options/golang.go index 10539d79c..c811a34c3 100644 --- a/cmd/syft/internal/options/golang.go +++ b/cmd/syft/internal/options/golang.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/anchore/clio" + "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/pkg/cataloger/golang" ) @@ -17,10 +18,12 @@ type golangConfig struct { NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"` MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"` UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"` + CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"` } var _ interface { clio.FieldDescriber + clio.PostLoader } = (*golangConfig)(nil) func (o *golangConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { @@ -39,12 +42,19 @@ if unset this defaults to $GONOPROXY`) always show (devel) as the version. Use these options to control heuristics to guess a more accurate version from the binary.`) descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`) + descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are: +"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`) descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`) - descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version + descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version (e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`) descriptions.Add(&o.MainModuleVersion.FromContents, `search for semver-like strings in the binary contents`) } +func (o *golangConfig) PostLoad() error { + o.CaptureSymbols = o.CaptureSymbols.Parse() + return nil +} + type golangMainModuleVersionConfig struct { FromLDFlags bool `json:"from-ld-flags" yaml:"from-ld-flags" mapstructure:"from-ld-flags"` FromContents bool `json:"from-contents" yaml:"from-contents" mapstructure:"from-contents"` @@ -67,5 +77,6 @@ func defaultGolangConfig() golangConfig { FromBuildSettings: def.MainModuleVersion.FromBuildSettings, }, UsePackagesLib: nil, // this defaults to true, which is the API default + CaptureSymbols: def.CaptureSymbols, } } diff --git a/cmd/syft/internal/options/golang_test.go b/cmd/syft/internal/options/golang_test.go new file mode 100644 index 000000000..643349904 --- /dev/null +++ b/cmd/syft/internal/options/golang_test.go @@ -0,0 +1,57 @@ +package options + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/cataloging" +) + +func Test_golangConfig_PostLoad(t *testing.T) { + tests := []struct { + name string + cfg golangConfig + expected cataloging.SymbolScope + wantErr assert.ErrorAssertionFunc + }{ + { + name: "normalize all", + cfg: golangConfig{CaptureSymbols: "all"}, + expected: cataloging.SymbolScopeAll, + }, + { + name: "normalize stdlib", + cfg: golangConfig{CaptureSymbols: "stdlib"}, + expected: cataloging.SymbolScopeStdlib, + }, + { + name: "empty defaults to none", + cfg: golangConfig{CaptureSymbols: ""}, + expected: cataloging.SymbolScopeNone, + }, + { + name: "invalid value defaults to none", + cfg: golangConfig{CaptureSymbols: "bogus"}, + expected: cataloging.SymbolScopeNone, + }, + { + name: "boolean spellings default to none", + cfg: golangConfig{CaptureSymbols: "true"}, + expected: cataloging.SymbolScopeNone, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = assert.NoError + } + err := tt.cfg.PostLoad() + tt.wantErr(t, err) + if err != nil { + return + } + assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols) + }) + } +} diff --git a/internal/capabilities/appconfig.yaml b/internal/capabilities/appconfig.yaml index e073dd32c..295df39df 100644 --- a/internal/capabilities/appconfig.yaml +++ b/internal/capabilities/appconfig.yaml @@ -14,6 +14,8 @@ application: # AUTO-GENERATED - application-level config keys description: treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package - key: dotnet.relax-dll-claims-when-bundling-detected description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack) + - key: golang.capture-symbols + description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)' - key: golang.local-mod-cache-dir description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod - key: golang.local-vendor-dir diff --git a/internal/constants.go b/internal/constants.go index 0efb094e2..b21c5adda 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,7 +3,7 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.1.8" + JSONSchemaVersion = "16.1.9" // Changelog // 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). @@ -15,4 +15,5 @@ const ( // 16.1.6 - add Dependencies to ElixirMixLockEntry metadata // 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger // 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support + // 16.1.9 - add Symbols to GolangBinaryBuildinfoEntry metadata ) diff --git a/schema/json/schema-16.1.9.json b/schema/json/schema-16.1.9.json new file mode 100644 index 000000000..e1363bf94 --- /dev/null +++ b/schema/json/schema-16.1.9.json @@ -0,0 +1,4520 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.1.9/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object", + "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman." + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "AppleAppBundleEntry": { + "properties": { + "bundleIdentifier": { + "type": "string", + "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")" + }, + "name": { + "type": "string", + "description": "Name is the short name of the bundle" + }, + "displayName": { + "type": "string", + "description": "DisplayName is the user-facing name of the bundle" + }, + "executable": { + "type": "string", + "description": "Executable is the name of the executable within the bundle" + }, + "shortVersion": { + "type": "string", + "description": "ShortVersion is the release (marketing) version of the bundle" + }, + "version": { + "type": "string", + "description": "Version is the build version of the bundle, which often differs from the short version" + }, + "packageType": { + "type": "string", + "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)" + }, + "supportedPlatforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")" + }, + "minimumSystemVersion": { + "type": "string", + "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle" + }, + "minimumOSVersion": { + "type": "string", + "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)" + }, + "copyright": { + "type": "string", + "description": "Copyright is the human-readable copyright notice for the bundle" + }, + "platformName": { + "type": "string", + "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")" + }, + "sdkName": { + "type": "string", + "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")" + } + }, + "type": "object", + "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string", + "description": "Value is the CPE string identifier." + }, + "source": { + "type": "string", + "description": "Source is the source where this CPE was obtained or generated from." + } + }, + "type": "object", + "required": [ + "cpe" + ], + "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases." + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, + "Descriptor": { + "properties": { + "name": { + "type": "string", + "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")." + }, + "version": { + "type": "string", + "description": "Version is the version of the tool that generated this SBOM." + }, + "configuration": { + "description": "Configuration contains the tool configuration used during SBOM generation." + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation." + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array", + "description": "Artifacts is the list of packages discovered and placed into the catalog" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array", + "description": "note: must have omitempty" + }, + "source": { + "$ref": "#/$defs/Source", + "description": "Source represents the original object that was cataloged" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease", + "description": "Distro represents the Linux distribution that was detected from the source" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor", + "description": "Descriptor is a block containing self-describing information about syft" + }, + "schema": { + "$ref": "#/$defs/Schema", + "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "type": { + "type": "string", + "description": "Type is type of entry could be package or project for internal refs" + }, + "executables": { + "additionalProperties": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing." + }, + "appCpe": { + "type": "string", + "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages." + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for this file within the SBOM." + }, + "location": { + "$ref": "#/$defs/Coordinates", + "description": "Location is the file path and layer information where this file was found." + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry", + "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type." + }, + "contents": { + "type": "string", + "description": "Contents is the file contents for small files." + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains cryptographic hashes of the file contents." + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array", + "description": "Licenses contains license information discovered within this file." + }, + "executable": { + "$ref": "#/$defs/Executable", + "description": "Executable contains executable metadata if this file is a binary." + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Unknowns contains unknown fields for forward compatibility." + } + }, + "type": "object", + "required": [ + "id", + "location" + ], + "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages." + }, + "FileLicense": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or text as found in the file." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence", + "description": "Evidence contains supporting evidence for this license detection." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ], + "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression." + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer", + "description": "Confidence is the confidence score for this license detection (0-100)." + }, + "offset": { + "type": "integer", + "description": "Offset is the byte offset where the license text starts in the file." + }, + "extent": { + "type": "integer", + "description": "Extent is the length of the license text in bytes." + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ], + "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level." + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer", + "description": "Mode is the Unix file permission mode in octal format." + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")." + }, + "linkDestination": { + "type": "string", + "description": "LinkDestination is the target path for symbolic links." + }, + "userID": { + "type": "integer", + "description": "UserID is the file owner user ID." + }, + "groupID": { + "type": "integer", + "description": "GroupID is the file owner group ID." + }, + "mimeType": { + "type": "string", + "description": "MIMEType is the MIME type of the file contents." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ], + "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file." + }, + "GgufFileHeader": { + "properties": { + "ggufVersion": { + "type": "integer", + "description": "GGUFVersion is the GGUF format version (e.g., 3)" + }, + "fileSize": { + "type": "integer", + "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")" + }, + "quantization": { + "type": "string", + "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")" + }, + "parameters": { + "type": "integer", + "description": "Parameters is the number of model parameters (if present in header)" + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensors in the model" + }, + "header": { + "type": "object", + "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication." + }, + "metadataHash": { + "type": "string", + "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames." + }, + "parts": { + "items": { + "$ref": "#/$defs/GgufFileHeader" + }, + "type": "array", + "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)." + } + }, + "type": "object", + "required": [ + "ggufVersion", + "tensorCount" + ], + "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file." + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + }, + "symbols": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field." + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptBunLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of runtime dependencies and their version specifiers" + }, + "optionalDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "OptionalDependencies is a map of optional dependencies and their version specifiers" + }, + "peerDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "PeerDependencies is a map of peer dependencies and their version specifiers" + }, + "bin": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Bin is a map of binary names to the paths they are installed to" + }, + "os": { + "type": "string", + "description": "OS is the operating system constraint for the package (e.g. \"darwin\")" + }, + "cpu": { + "type": "string", + "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies", + "optionalDependencies", + "peerDependencies", + "bin", + "os", + "cpu" + ], + "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\"" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptPnpmLockEntry": { + "properties": { + "resolution": { + "$ref": "#/$defs/PnpmLockResolution", + "description": "Resolution is the resolution information for the package" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolution", + "dependencies" + ], + "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or expression as found." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URLs are URLs where license text or information can be found." + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array", + "description": "Locations are file locations where this license was discovered." + }, + "contents": { + "type": "string", + "description": "Contents is the full license text content." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ], + "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations." + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string", + "description": "PrettyName is a human-readable operating system name with version." + }, + "name": { + "type": "string", + "description": "Name is the operating system name without version information." + }, + "id": { + "type": "string", + "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")." + }, + "idLike": { + "$ref": "#/$defs/IDLikes", + "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from." + }, + "version": { + "type": "string", + "description": "Version is the operating system version including codename if available." + }, + "versionID": { + "type": "string", + "description": "VersionID is the operating system version number or identifier." + }, + "versionCodename": { + "type": "string", + "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")." + }, + "buildID": { + "type": "string", + "description": "BuildID is a build identifier for the operating system." + }, + "imageID": { + "type": "string", + "description": "ImageID is an identifier for container or cloud images." + }, + "imageVersion": { + "type": "string", + "description": "ImageVersion is the version for container or cloud images." + }, + "variant": { + "type": "string", + "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")." + }, + "variantID": { + "type": "string", + "description": "VariantID is the lower-case operating system variant identifier." + }, + "homeURL": { + "type": "string", + "description": "HomeURL is the homepage URL for the operating system." + }, + "supportURL": { + "type": "string", + "description": "SupportURL is the support or help URL for the operating system." + }, + "bugReportURL": { + "type": "string", + "description": "BugReportURL is the bug reporting URL for the operating system." + }, + "privacyPolicyURL": { + "type": "string", + "description": "PrivacyPolicyURL is the privacy policy URL for the operating system." + }, + "cpeName": { + "type": "string", + "description": "CPEName is the Common Platform Enumeration name for the operating system." + }, + "supportEnd": { + "type": "string", + "description": "SupportEnd is the end of support date or version identifier." + }, + "extendedSupport": { + "type": "boolean", + "description": "ExtendedSupport indicates whether extended security or support is available." + } + }, + "type": "object", + "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files." + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Arbitrary key-value pairs that can be used to annotate a location" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/AppleAppBundleEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GgufFileHeader" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptBunLockEntry" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptPnpmLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/VcpkgManifest" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PnpmLockResolution": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "integrity" + ], + "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmFileEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the file download URL" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest is the hash digest of the file hosted at the URL" + } + }, + "type": "object", + "required": [ + "url", + "digest" + ] + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests (for the base package without extras)" + }, + "marker": { + "type": "string", + "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment" + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications for the base package (without extras)" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPdmLockExtraVariant" + }, + "type": "array", + "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)" + } + }, + "type": "object", + "required": [ + "summary", + "files" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPdmLockExtraVariant": { + "properties": { + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependencies specific to this extras variant" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files specific to this variant (only populated if different from base)" + }, + "marker": { + "type": "string", + "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")" + } + }, + "type": "object", + "required": [ + "extras" + ], + "description": "PythonPdmLockExtraVariant represents a specific extras combination variant within a PDM lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string", + "description": "Parent is the ID of the parent artifact in this relationship." + }, + "child": { + "type": "string", + "description": "Child is the ID of the child artifact in this relationship." + }, + "type": { + "type": "string", + "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")." + }, + "metadata": { + "description": "Metadata contains additional relationship-specific metadata." + } + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ], + "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package." + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "Schema": { + "properties": { + "version": { + "type": "string", + "description": "Version is the JSON schema version for this document format." + }, + "url": { + "type": "string", + "description": "URL is the URL to the JSON schema definition document." + } + }, + "type": "object", + "required": [ + "version", + "url" + ], + "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format." + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for the analyzed source artifact." + }, + "name": { + "type": "string", + "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)." + }, + "version": { + "type": "string", + "description": "Version is the version of the analyzed artifact (e.g., image tag)." + }, + "supplier": { + "type": "string", + "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance." + }, + "type": { + "type": "string", + "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")." + }, + "metadata": { + "description": "Metadata contains additional source-specific metadata." + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "VcpkgManifest": { + "properties": { + "description": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)." + }, + "documentation": { + "type": "string", + "description": "Documentation is the URL to the package's documentation." + }, + "full-version": { + "type": "string", + "description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")." + }, + "version": { + "type": "string", + "description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")." + }, + "port-version": { + "type": "integer", + "description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version." + }, + "maintainers": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Maintainers are the people responsible for maintaining the vcpkg port." + }, + "name": { + "type": "string", + "description": "Name is the package name as declared in the manifest." + }, + "supports": { + "type": "string", + "description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")." + }, + "registry": { + "$ref": "#/$defs/VcpkgRegistryEntry", + "description": "Registry indicates where the package definition came from." + }, + "triplet": { + "type": "string", + "description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")." + } + }, + "type": "object", + "required": [ + "full-version", + "version", + "port-version", + "name" + ], + "description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package." + }, + "VcpkgRegistryEntry": { + "properties": { + "baseline": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "packages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "path": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "repository": { + "type": "string" + } + }, + "type": "object", + "required": [ + "kind" + ], + "description": "Matches definition of Vcpkg \"Registry\"." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 1fc811850..e1363bf94 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.1.8/document", + "$id": "anchore.io/schema/syft/json/16.1.9/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -1653,6 +1653,13 @@ }, "type": "array", "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + }, + "symbols": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package." } }, "type": "object", @@ -4394,39 +4401,47 @@ "items": { "type": "string" }, - "type": "array" + "type": "array", + "description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)." }, "documentation": { - "type": "string" + "type": "string", + "description": "Documentation is the URL to the package's documentation." }, "full-version": { - "type": "string" + "type": "string", + "description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")." }, "version": { - "type": "string" + "type": "string", + "description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")." }, "port-version": { - "type": "integer" + "type": "integer", + "description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version." }, "maintainers": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "description": "Maintainers are the people responsible for maintaining the vcpkg port." }, "name": { - "type": "string" + "type": "string", + "description": "Name is the package name as declared in the manifest." }, "supports": { - "type": "string" + "type": "string", + "description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")." }, "registry": { "$ref": "#/$defs/VcpkgRegistryEntry", - "description": "to show where it came from" + "description": "Registry indicates where the package definition came from." }, "triplet": { "type": "string", - "description": "found by looking at build folder to find target. ex. \"x64-linux\"" + "description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")." } }, "type": "object", @@ -4436,7 +4451,7 @@ "port-version", "name" ], - "description": "used for metadata." + "description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package." }, "VcpkgRegistryEntry": { "properties": { diff --git a/syft/cataloging/symbols.go b/syft/cataloging/symbols.go new file mode 100644 index 000000000..4e892fcee --- /dev/null +++ b/syft/cataloging/symbols.go @@ -0,0 +1,28 @@ +package cataloging + +import "strings" + +// SymbolScope controls which packages get function symbols (from a binary's symbol table) attached to their metadata. +type SymbolScope string + +const ( + // SymbolScopeNone disables symbol capture entirely. + SymbolScopeNone SymbolScope = "none" + + // SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols. + SymbolScopeStdlib SymbolScope = "stdlib" + + // SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package. + SymbolScopeAll SymbolScope = "all" +) + +// Parse normalizes a SymbolScope, treating empty (unset) and unrecognized values as SymbolScopeNone. +func (s SymbolScope) Parse() SymbolScope { + switch strings.ToLower(strings.TrimSpace(string(s))) { + case string(SymbolScopeAll): + return SymbolScopeAll + case string(SymbolScopeStdlib): + return SymbolScopeStdlib + } + return SymbolScopeNone +} diff --git a/syft/cataloging/symbols_test.go b/syft/cataloging/symbols_test.go new file mode 100644 index 000000000..307119a15 --- /dev/null +++ b/syft/cataloging/symbols_test.go @@ -0,0 +1,30 @@ +package cataloging + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_SymbolScope_Parse(t *testing.T) { + tests := []struct { + input string + expected SymbolScope + }{ + {"all", SymbolScopeAll}, + {"ALL", SymbolScopeAll}, + {" all ", SymbolScopeAll}, + {"stdlib", SymbolScopeStdlib}, + {"Stdlib", SymbolScopeStdlib}, + {"none", SymbolScopeNone}, + {"", SymbolScopeNone}, + {"true", SymbolScopeNone}, + {"false", SymbolScopeNone}, + {"bogus", SymbolScopeNone}, + } + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + assert.Equal(t, test.expected, SymbolScope(test.input).Parse()) + }) + } +} diff --git a/syft/pkg/cataloger/golang/capabilities.yaml b/syft/pkg/cataloger/golang/capabilities.yaml index b0e55805c..01e24b146 100644 --- a/syft/pkg/cataloger/golang/capabilities.yaml +++ b/syft/pkg/cataloger/golang/capabilities.yaml @@ -24,6 +24,9 @@ configs: # AUTO-GENERATED - config structs and their fields - key: NoProxy description: NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars. app_key: golang.no-proxy + - key: CaptureSymbols + description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib). + app_key: golang.capture-symbols catalogers: - ecosystem: go # MANUAL name: go-module-binary-cataloger # AUTO-GENERATED diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index d52c210c0..f51dc8955 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -26,10 +26,11 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger { // NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler. func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger { + c := newGoBinaryCataloger(opts) return generic.NewCataloger(binaryCatalogerName). WithParserByMimeTypes( - newGoBinaryCataloger(opts).parseGoBinary, + c.parseGoBinary, mimetype.ExecutableMIMETypeSet.List()..., ). - WithResolvingProcessors(stdlibProcessor) + WithResolvingProcessors(c.stdlibProcessor) } diff --git a/syft/pkg/cataloger/golang/config.go b/syft/pkg/cataloger/golang/config.go index c90767275..228cd67b1 100644 --- a/syft/pkg/cataloger/golang/config.go +++ b/syft/pkg/cataloger/golang/config.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/go-homedir" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/cataloging" ) const ( @@ -49,6 +50,11 @@ type CatalogerConfig struct { MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"` + // CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are + // "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib). + // app-config: golang.capture-symbols + CaptureSymbols cataloging.SymbolScope `yaml:"capture-symbols" json:"capture-symbols" mapstructure:"capture-symbols"` + // Whether to use the golang.org/x/tools/go/packages, which executes golang tooling found on the path in addition to potential network access UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"` } @@ -76,6 +82,7 @@ func DefaultCatalogerConfig() CatalogerConfig { UsePackagesLib: true, MainModuleVersion: DefaultMainModuleVersionConfig(), LocalModCacheDir: defaultGoModDir(), + CaptureSymbols: cataloging.SymbolScopeNone, } // first process the proxy settings @@ -184,6 +191,11 @@ func (g CatalogerConfig) WithMainModuleVersion(input MainModuleVersionConfig) Ca return g } +func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) CatalogerConfig { + g.CaptureSymbols = input + return g +} + func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig { g.UsePackagesLib = useLib return g diff --git a/syft/pkg/cataloger/golang/config_test.go b/syft/pkg/cataloger/golang/config_test.go index 4b563e656..1b16a190a 100644 --- a/syft/pkg/cataloger/golang/config_test.go +++ b/syft/pkg/cataloger/golang/config_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/go-homedir" + "github.com/anchore/syft/syft/cataloging" ) func Test_Config(t *testing.T) { @@ -58,6 +59,7 @@ func Test_Config(t *testing.T) { NoProxy: []string{"my.private", "no.proxy"}, MainModuleVersion: DefaultMainModuleVersionConfig(), UsePackagesLib: true, + CaptureSymbols: cataloging.SymbolScopeNone, }, }, { @@ -86,6 +88,7 @@ func Test_Config(t *testing.T) { NoProxy: []string{"alt.no.proxy"}, MainModuleVersion: DefaultMainModuleVersionConfig(), UsePackagesLib: true, + CaptureSymbols: cataloging.SymbolScopeNone, }, }, } diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go index f689c1a40..4e1e39a80 100644 --- a/syft/pkg/cataloger/golang/package.go +++ b/syft/pkg/cataloger/golang/package.go @@ -45,7 +45,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi return p } -func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments []string) pkg.GolangBinaryBuildinfoEntry { +func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments, symbols []string) pkg.GolangBinaryBuildinfoEntry { if dep.Replace != nil { dep = dep.Replace } @@ -58,6 +58,7 @@ func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture st MainModule: mainModule, GoCryptoSettings: cryptoSettings, GoExperiments: experiments, + Symbols: symbols, } } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 9cb25337f..8553ec4be 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -13,6 +13,7 @@ import ( "runtime/debug" "slices" "strings" + "sync" "time" "golang.org/x/mod/module" @@ -20,6 +21,7 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" @@ -49,15 +51,45 @@ const devel = "(devel)" type goBinaryCataloger struct { licenseResolver goLicenseResolver mainModuleVersion MainModuleVersionConfig + symbolScope cataloging.SymbolScope + + // stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the + // binary's location), populated during parsing and consumed by stdlibProcessor when it builds the + // synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run concurrently. + stdlibSymbols map[file.Coordinates][]string + stdlibSymbolsMu sync.Mutex } func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger { return &goBinaryCataloger{ licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts), mainModuleVersion: opts.MainModuleVersion, + symbolScope: opts.CaptureSymbols, + stdlibSymbols: make(map[file.Coordinates][]string), } } +// recordStdlibSymbols merges the standard-library symbols discovered for a binary location so the +// stdlib processor can attach them to the synthetic stdlib package. +func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols []string) { + if len(symbols) == 0 { + return + } + c.stdlibSymbolsMu.Lock() + defer c.stdlibSymbolsMu.Unlock() + merged := slices.Concat(c.stdlibSymbols[coord], symbols) + slices.Sort(merged) + c.stdlibSymbols[coord] = slices.Compact(merged) +} + +// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a copy +// so callers cannot alias (and later mutate or race on) the map's internal slice. +func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string { + c.stdlibSymbolsMu.Lock() + defer c.stdlibSymbolsMu.Unlock() + return slices.Clone(c.stdlibSymbols[coord]) +} + // parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler. func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package @@ -68,7 +100,7 @@ func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Res } defer internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) - mods, errs := scanFile(reader.Location, unionReader) + mods, errs := scanFile(reader.Location, unionReader, c.symbolScope != cataloging.SymbolScopeNone) var rels []artifact.Relationship for _, mod := range mods { @@ -128,6 +160,15 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re mod.Main = createMainModuleFromPath(mod) } + symbolsByModule, stdlibSymbols := moduleSymbols(mod.symbols, &mod.Main, mod.Deps) + c.recordStdlibSymbols(location.Coordinates, stdlibSymbols) + + if c.symbolScope != cataloging.SymbolScopeAll { + // only the "all" scope attaches per-module symbols; for the "stdlib" scope we keep just the + // recorded stdlib symbols. nil map lookups below then yield nil symbol lists for each module. + symbolsByModule = nil + } + var pkgs []pkg.Package for _, dep := range mod.Deps { if dep == nil { @@ -147,6 +188,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re nil, mod.cryptoSettings, experiments, + symbolsByModule[dep.Path], ) p := c.newGoBinaryPackage( @@ -164,7 +206,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re return nil, pkgs } - main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader) + main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader, symbolsByModule[mod.Main.Path]) return &main, pkgs } @@ -179,7 +221,7 @@ func missingMainModule(mod *extendedBuildInfo) bool { return mod.Main == moduleFromPartialPackageBuild } -func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package { +func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols []string) pkg.Package { gbs := getBuildSettings(mod.Settings) lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version) gover, experiments := getExperimentsFromVersion(mod.GoVersion) @@ -192,6 +234,7 @@ func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file gbs, mod.cryptoSettings, experiments, + symbols, ) if mod.Main.Version == devel { @@ -387,7 +430,7 @@ func getExperimentsFromVersion(version string) (string, []string) { version, rest, ok := strings.Cut(version, " ") if ok { // Assume they may add more non-version chunks in the future, so only look for "X:". - for _, chunk := range strings.Split(rest, " ") { + for chunk := range strings.SplitSeq(rest, " ") { if strings.HasPrefix(rest, "X:") { csv := strings.TrimPrefix(chunk, "X:") experiments = append(experiments, strings.Split(csv, ",")...) diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index d0fda31fe..648068822 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/fileresolver" "github.com/anchore/syft/syft/internal/unionreader" @@ -1422,3 +1423,71 @@ type alwaysErrorReader struct{} func (alwaysErrorReader) Read(_ []byte) (int, error) { return 0, errors.New("read from always error reader") } + +func Test_buildGoPkgInfo_symbolScope(t *testing.T) { + location := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"}) + + // the symbols a binary would carry once scanFile has extracted them: one main-package symbol, one + // dependency symbol, and one standard-library symbol. For the "none" scope scanFile never runs, so the + // build info carries no symbols at all. + populatedSymbols := []binarySymbol{ + {packagePath: "main", name: "main.main"}, + {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, + {packagePath: "net/http", name: "net/http.(*Client).Do"}, + } + + tests := []struct { + name string + scope cataloging.SymbolScope + symbols []binarySymbol + wantMainSyms []string + wantDepSyms []string + wantStdlibSyms []string + }{ + { + name: "none captures nothing", + scope: cataloging.SymbolScopeNone, + symbols: nil, + }, + { + name: "stdlib captures only the stdlib package", + scope: cataloging.SymbolScopeStdlib, + symbols: populatedSymbols, + wantStdlibSyms: []string{"net/http.(*Client).Do"}, + }, + { + name: "all captures module and stdlib packages", + scope: cataloging.SymbolScopeAll, + symbols: populatedSymbols, + wantMainSyms: []string{"main.main"}, + wantDepSyms: []string{"github.com/foo/bar.Parse"}, + wantStdlibSyms: []string{"net/http.(*Client).Do"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mod := &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: "go1.22.0", + Main: debug.Module{Path: "github.com/anchore/syft", Version: "v1.0.0"}, + Deps: []*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}}, + }, + arch: "amd64", + symbols: tt.symbols, + } + + c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope}) + reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(""))) + require.NoError(t, err) + + mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), fileresolver.Empty{}, location, mod, mod.arch, reader) + require.NotNil(t, mainPkg) + require.Len(t, pkgs, 1) + + assert.Equal(t, tt.wantMainSyms, mainPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "main module symbols") + assert.Equal(t, tt.wantDepSyms, pkgs[0].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "dependency symbols") + assert.Equal(t, tt.wantStdlibSyms, c.stdlibSymbolsFor(location.Coordinates), "recorded stdlib symbols") + }) + } +} diff --git a/syft/pkg/cataloger/golang/scan_binary.go b/syft/pkg/cataloger/golang/scan_binary.go index cc41d7190..84fd6d2b2 100644 --- a/syft/pkg/cataloger/golang/scan_binary.go +++ b/syft/pkg/cataloger/golang/scan_binary.go @@ -18,10 +18,11 @@ type extendedBuildInfo struct { *debug.BuildInfo cryptoSettings []string arch string + symbols []binarySymbol } // scanFile scans file to try to report the Go and module versions. -func scanFile(location file.Location, reader unionreader.UnionReader) ([]*extendedBuildInfo, error) { +func scanFile(location file.Location, reader unionreader.UnionReader, captureSymbols bool) ([]*extendedBuildInfo, error) { // NOTE: multiple readers are returned to cover universal binaries, which are files // with more than one binary readers, errs := unionreader.GetReaders(reader) @@ -61,7 +62,18 @@ func scanFile(location file.Location, reader unionreader.UnionReader) ([]*extend } } - builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch}) + var symbols []binarySymbol + if captureSymbols { + symbols, err = getSymbols(r) + if err != nil { + log.WithFields("file", location.RealPath, "error", err).Trace("unable to read golang symbol info") + // don't skip this build info. + // we can still catalog packages, even if we can't get the symbol information + errs = unknown.Appendf(errs, location, "unable to read golang symbol info: %w", err) + } + } + + builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch, symbols: symbols}) } return builds, errs } diff --git a/syft/pkg/cataloger/golang/stdlib_package.go b/syft/pkg/cataloger/golang/stdlib_package.go index b1c7ea67a..1743372e5 100644 --- a/syft/pkg/cataloger/golang/stdlib_package.go +++ b/syft/pkg/cataloger/golang/stdlib_package.go @@ -12,12 +12,12 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { - compilerPkgs, newRelationships := stdlibPackageAndRelationships(ctx, pkgs) +func (c *goBinaryCataloger) stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { + compilerPkgs, newRelationships := c.stdlibPackageAndRelationships(ctx, pkgs) return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err } -func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) { +func (c *goBinaryCataloger) stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) { var goCompilerPkgs []pkg.Package var relationships []artifact.Relationship totalLocations := file.NewLocationSet() @@ -33,7 +33,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p continue } - stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations) + stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations, c.stdlibSymbolsFor(location.Coordinates)) if stdLibPkg == nil { continue } @@ -50,7 +50,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p return goCompilerPkgs, relationships } -func newGoStdLib(ctx context.Context, version string, location file.LocationSet) *pkg.Package { +func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols []string) *pkg.Package { stdlibCpe, err := generateStdlibCpe(version) if err != nil { return nil @@ -66,6 +66,7 @@ func newGoStdLib(ctx context.Context, version string, location file.LocationSet) Type: pkg.GoModulePkg, Metadata: pkg.GolangBinaryBuildinfoEntry{ GoCompiledVersion: version, + Symbols: symbols, }, } goCompilerPkg.SetID() @@ -79,10 +80,10 @@ func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) { // we also need to trim starting from the first + to // correctly extract potential rc candidate information for cpe generation - // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned - after, _, found := strings.Cut("+", version) + // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found version is unchanged + before, _, found := strings.Cut(version, "+") if found { - version = after + version = before } // extracting and diff --git a/syft/pkg/cataloger/golang/stdlib_package_test.go b/syft/pkg/cataloger/golang/stdlib_package_test.go index 395f626cd..3c4e8ff75 100644 --- a/syft/pkg/cataloger/golang/stdlib_package_test.go +++ b/syft/pkg/cataloger/golang/stdlib_package_test.go @@ -88,7 +88,8 @@ func Test_stdlibPackageAndRelationships(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, tt.pkgs) + c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)} + gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, tt.pkgs) assert.Len(t, gotPkgs, tt.wantPkgs) assert.Len(t, gotRels, tt.wantRels) }) @@ -137,7 +138,8 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) { Type: artifact.DependencyOfRelationship, } - gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, []pkg.Package{p}) + c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)} + gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, []pkg.Package{p}) require.Len(t, gotPkgs, 1) gotPkg := gotPkgs[0] diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go new file mode 100644 index 000000000..3d553a739 --- /dev/null +++ b/syft/pkg/cataloger/golang/symbols.go @@ -0,0 +1,332 @@ +package golang + +import ( + "bytes" + "debug/elf" + "debug/gosym" + "debug/macho" + "encoding/binary" + "fmt" + "io" + "runtime/debug" + "slices" + "strings" +) + +// mainPackage is the import path the linker assigns to the binary's main package. +const mainPackage = "main" + +// binarySymbol represents a single function symbol extracted from a go binary's pclntab. +type binarySymbol struct { + // packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz") + packagePath string + + // name is the fully qualified symbol name (e.g. "github.com/foo/bar/internal/baz.(*Type).Method") + name string +} + +// getSymbols extracts all function symbols from the pclntab of a go binary. The pclntab is required by the +// go runtime (for panic tracebacks and GC), so it is present even in binaries built with -ldflags="-s -w". +func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) { + defer func() { + if r := recover(); r != nil { + // the gosym package can panic on malformed pclntab data + syms = nil + err = fmt.Errorf("recovered from panic while reading pclntab: %v", r) + } + }() + + pclntab, textStart, err := readPclntab(r) + if err != nil { + return nil, err + } + + table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart)) + if err != nil { + return nil, fmt.Errorf("unable to parse pclntab: %w", err) + } + + seen := make(map[string]struct{}) + for _, fn := range table.Funcs { + if fn.Sym == nil || isCompilerGeneratedName(fn.Name) { + continue + } + seen[fn.Name] = struct{}{} + syms = append(syms, binarySymbol{ + packagePath: fn.PackageName(), + name: fn.Name, + }) + } + + // debug/gosym only exposes top-level functions; functions that the compiler inlined into their + // callers are absent from table.Funcs even though their names are recorded in the pclntab funcname + // table (used to reconstruct inlined frames in tracebacks). Recover those names so that a + // vulnerable-but-inlined function (e.g. a small stdlib wrapper) is still reported as present. + for _, name := range funcNameTable(pclntab) { + if _, ok := seen[name]; ok { + continue + } + pkgPath := packagePathFromSymbolName(name) + if pkgPath == "" { + continue + } + seen[name] = struct{}{} + syms = append(syms, binarySymbol{packagePath: pkgPath, name: name}) + } + + return syms, nil +} + +// packagePathFromSymbolName derives the owning package import path from a fully qualified symbol name. +// The package path is everything up to the first "." that follows the final "/" — e.g. +// "path/filepath.IsLocal" -> "path/filepath" and "golang.org/x/net/html.(*Tokenizer).Next" -> +// "golang.org/x/net/html". Returns "" when the name has no package-qualifying dot or is compiler-generated. +func packagePathFromSymbolName(name string) string { + if isCompilerGeneratedName(name) { + return "" + } + name = nameWithoutTypeArgs(name) + slash := strings.LastIndex(name, "/") + dot := strings.IndexByte(name[slash+1:], '.') + if dot < 0 { + return "" + } + return name[:slash+1+dot] +} + +// nameWithoutTypeArgs strips the type-argument portion from an instantiated generic symbol name, e.g. +// "foo/bar.Do[net/url.Values]" -> "foo/bar.Do". The slashes and dots inside the brackets would otherwise +// corrupt package-path derivation (yielding "foo/bar.Do[net" for the example above). Mirrors +// debug/gosym's (*Sym).nameWithoutInst. +func nameWithoutTypeArgs(name string) string { + start := strings.IndexByte(name, '[') + if start < 0 { + return name + } + end := strings.LastIndexByte(name, ']') + if end < 0 { + // malformed: an opening bracket should always have a closing one + return name + } + return name[:start] + name[end+1:] +} + +// oldStyleCompilerGeneratedPrefixes match compiler/linker-generated symbols from toolchains older than +// go1.20, which used "." where newer toolchains use ":" (e.g. "go.buildid" is now "go:buildid", +// "go.type.*" is now "go:type.*"). These must be prefix (not substring) matches, and a bare "go." prefix +// is not enough: legitimate module paths such as "go.uber.org/zap" also start with "go.". +var oldStyleCompilerGeneratedPrefixes = []string{ + "go.buildid", + "go.builtin.", + "go.constinfo.", + "go.cuinfo.", + "go.func.", + "go.importpath.", + "go.info.", + "go.interface.", + "go.itab.", + "go.itablink.", + "go.map.", + "go.shape.", + "go.string.", + "go.type.", + "go.typelink.", + "type.", +} + +// isCompilerGeneratedName reports whether a symbol name was synthesized by the compiler or linker rather +// than declared in Go source. Since go1.20 these names contain ':' or '..' (e.g. "type:.eq.*", +// "go:string.*") — byte sequences that never appear in a real Go import path or identifier. Older +// toolchains used '.' as the separator (e.g. "go.type.*", "type..hash.*"), which is matched against the +// known reserved prefixes. Such names belong to no package and are dropped rather than mis-attributed +// (e.g. bucketed under a bogus "type" stdlib package). +func isCompilerGeneratedName(name string) bool { + if strings.Contains(name, ":") || strings.Contains(name, "..") { + return true + } + for _, prefix := range oldStyleCompilerGeneratedPrefixes { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} + +// funcNameTable returns every function name recorded in the pclntab's funcname table, including the +// names of inlined functions that debug/gosym does not expose. It parses the pclntab header for the +// Go 1.16+ layouts; on any unrecognized layout or out-of-bounds offset it returns nil (fail-soft), so +// callers fall back to the debug/gosym function set. See the runtime's pcHeader / moduledata layout. +func funcNameTable(pclntab []byte) []string { + if len(pclntab) < 8 { + return nil + } + + magic := binary.LittleEndian.Uint32(pclntab[0:4]) + // the field before funcnameOffset is textStart, which exists in the 1.18+ headers but not 1.16/1.17 + var hasTextStart bool + switch magic { + case 0xfffffff1, 0xfffffff0: // go1.20+, go1.18/1.19 + hasTextStart = true + case 0xfffffffa: // go1.16/1.17 + hasTextStart = false + default: + return nil + } + + ptrSize := int(pclntab[7]) + if ptrSize != 4 && ptrSize != 8 { + return nil + } + + readWord := func(idx int) (uint64, bool) { + off := 8 + idx*ptrSize + if off+ptrSize > len(pclntab) { + return 0, false + } + if ptrSize == 8 { + return binary.LittleEndian.Uint64(pclntab[off : off+8]), true + } + return uint64(binary.LittleEndian.Uint32(pclntab[off : off+4])), true + } + + // header words after (nfunc, nfiles): [textStart,] funcnameOffset, cuOffset, ... + funcnameIdx := 2 + if hasTextStart { + funcnameIdx = 3 + } + funcnameOffset, ok1 := readWord(funcnameIdx) + cuOffset, ok2 := readWord(funcnameIdx + 1) + if !ok1 || !ok2 { + return nil + } + + start, end := int(funcnameOffset), int(cuOffset) + if start < 0 || end > len(pclntab) || start >= end { + return nil + } + + var names []string + for raw := range bytes.SplitSeq(pclntab[start:end], []byte{0}) { + if len(raw) == 0 { + continue + } + names = append(names, string(raw)) + } + return names +} + +// readPclntab locates the pclntab and the start address of the text segment within the binary. +func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) { + ident := make([]byte, 16) + if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil { + return nil, 0, errUnrecognizedFormat + } + + switch { + case strings.HasPrefix(string(ident), "\x7FELF"): + f, err := elf.NewFile(r) + if err != nil { + return nil, 0, fmt.Errorf("unable to parse ELF binary: %w", err) + } + sect := f.Section(".gopclntab") + if sect == nil { + return nil, 0, fmt.Errorf("no .gopclntab section found") + } + pclntab, err := sect.Data() + if err != nil { + return nil, 0, fmt.Errorf("unable to read .gopclntab section: %w", err) + } + text := f.Section(".text") + if text == nil { + return nil, 0, fmt.Errorf("no .text section found") + } + return pclntab, text.Addr, nil + case strings.HasPrefix(string(ident), "\xFE\xED\xFA") || strings.HasPrefix(string(ident[1:]), "\xFA\xED\xFE"): + f, err := macho.NewFile(r) + if err != nil { + return nil, 0, fmt.Errorf("unable to parse Mach-O binary: %w", err) + } + sect := f.Section("__gopclntab") + if sect == nil { + return nil, 0, fmt.Errorf("no __gopclntab section found") + } + pclntab, err := sect.Data() + if err != nil { + return nil, 0, fmt.Errorf("unable to read __gopclntab section: %w", err) + } + text := f.Section("__text") + if text == nil { + return nil, 0, fmt.Errorf("no __text section found") + } + return pclntab, text.Addr, nil + } + + // note: PE and XCOFF binaries do not place the pclntab in a dedicated section; locating it requires + // walking the symbol table for runtime.pclntab markers, which is not yet supported here + return nil, 0, errUnrecognizedFormat +} + +// moduleSymbols attributes each extracted symbol to the module that owns it (by longest module path prefix +// of the symbol's package path) and returns a sorted, deduplicated list of symbol names per module path. +// Symbols from the "main" package are attributed to the main module. Standard-library symbols (which belong +// to no module) are collected separately and returned as the second value so they can be attached to the +// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a +// recognizable stdlib import path are dropped. +func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string][]string, stdlib []string) { + if len(symbols) == 0 { + return nil, nil + } + + var modulePaths []string + if main != nil && main.Path != "" { + modulePaths = append(modulePaths, main.Path) + } + for _, dep := range deps { + if dep != nil && dep.Path != "" { + modulePaths = append(modulePaths, dep.Path) + } + } + + results := make(map[string][]string) + for _, sym := range symbols { + pkgPath := sym.packagePath + if pkgPath == mainPackage && main != nil { + // the linker renames the main package's import path to "main" + pkgPath = main.Path + } + + var best string + for _, modPath := range modulePaths { + if len(modPath) > len(best) && (pkgPath == modPath || strings.HasPrefix(pkgPath, modPath+"/")) { + best = modPath + } + } + if best == "" { + if pkgPath != mainPackage && isStandardImportPath(pkgPath) { + stdlib = append(stdlib, sym.name) + } + continue + } + results[best] = append(results[best], sym.name) + } + + for modPath, names := range results { + slices.Sort(names) + results[modPath] = slices.Compact(names) + } + if len(stdlib) > 0 { + slices.Sort(stdlib) + stdlib = slices.Compact(stdlib) + } + + return results, stdlib +} + +// isStandardImportPath reports whether path is a Go standard-library import path. This mirrors the rule +// the Go toolchain uses: a path is standard if the element before its first slash contains no dot (e.g. +// "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like +// "github.com/foo/bar" whose leading element is a domain name. +func isStandardImportPath(path string) bool { + first, _, _ := strings.Cut(path, "/") + return first != "" && !strings.Contains(first, ".") +} diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go new file mode 100644 index 000000000..703c3210a --- /dev/null +++ b/syft/pkg/cataloger/golang/symbols_test.go @@ -0,0 +1,201 @@ +package golang + +import ( + "debug/gosym" + "os" + "runtime" + "runtime/debug" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_moduleSymbols(t *testing.T) { + mainModule := &debug.Module{Path: "github.com/someorg/somecli"} + deps := []*debug.Module{ + {Path: "github.com/foo/bar"}, + {Path: "github.com/foo/bar/v2"}, + nil, + } + + tests := []struct { + name string + symbols []binarySymbol + expected map[string][]string + expectedStdlib []string + }{ + { + name: "no symbols", + symbols: nil, + expected: nil, + }, + { + name: "attribute symbols by longest module path prefix", + symbols: []binarySymbol{ + {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, + {packagePath: "github.com/foo/bar/internal/util", name: "github.com/foo/bar/internal/util.(*Helper).Do"}, + {packagePath: "github.com/foo/bar/v2", name: "github.com/foo/bar/v2.Parse"}, + }, + expected: map[string][]string{ + "github.com/foo/bar": { + "github.com/foo/bar.Parse", + "github.com/foo/bar/internal/util.(*Helper).Do", + }, + "github.com/foo/bar/v2": { + "github.com/foo/bar/v2.Parse", + }, + }, + }, + { + name: "main package symbols are attributed to the main module", + symbols: []binarySymbol{ + {packagePath: "main", name: "main.main"}, + {packagePath: "github.com/someorg/somecli/cmd", name: "github.com/someorg/somecli/cmd.Execute"}, + }, + expected: map[string][]string{ + "github.com/someorg/somecli": { + "github.com/someorg/somecli/cmd.Execute", + "main.main", + }, + }, + }, + { + name: "stdlib and runtime symbols are collected separately", + symbols: []binarySymbol{ + {packagePath: "runtime", name: "runtime.main"}, + {packagePath: "net/http", name: "net/http.(*Client).Do"}, + {packagePath: "internal/abi", name: "internal/abi.(*Type).Kind"}, + }, + expected: map[string][]string{}, + expectedStdlib: []string{ + "internal/abi.(*Type).Kind", + "net/http.(*Client).Do", + "runtime.main", + }, + }, + { + name: "duplicate symbols are deduplicated", + symbols: []binarySymbol{ + {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, + {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, + }, + expected: map[string][]string{ + "github.com/foo/bar": { + "github.com/foo/bar.Parse", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotByModule, gotStdlib := moduleSymbols(tt.symbols, mainModule, deps) + assert.Equal(t, tt.expected, gotByModule) + assert.Equal(t, tt.expectedStdlib, gotStdlib) + }) + } +} + +func Test_getSymbols(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PE binaries are not supported for symbol extraction") + } + + // the test executable is itself a go binary with a pclntab, which makes for a hermetic fixture + exe, err := os.Executable() + require.NoError(t, err) + + f, err := os.Open(exe) + require.NoError(t, err) + defer f.Close() + + symbols, err := getSymbols(f) + require.NoError(t, err) + require.NotEmpty(t, symbols) + + var foundRuntime, foundTesting bool + for _, sym := range symbols { + switch { + case sym.packagePath == "runtime" && sym.name == "runtime.main": + foundRuntime = true + case sym.packagePath == "testing" && sym.name == "testing.tRunner": + foundTesting = true + } + } + assert.True(t, foundRuntime, "expected to find runtime.main symbol") + assert.True(t, foundTesting, "expected to find testing.tRunner symbol") + + // the recovery loop relies on packagePathFromSymbolName, so confirm at least one recovered name is + // present that debug/gosym's table.Funcs does not surface directly (i.e. an inlined function). + require.True(t, hasInlinedOnlySymbol(t, exe, symbols), "expected to recover at least one inlined-only symbol") +} + +// hasInlinedOnlySymbol reports whether syms contains a function name that is absent from the raw +// debug/gosym function table for the same binary — i.e. a name that could only have come from the +// funcname-table recovery path. +func hasInlinedOnlySymbol(t *testing.T, exe string, syms []binarySymbol) bool { + t.Helper() + + f, err := os.Open(exe) + require.NoError(t, err) + defer f.Close() + + pclntab, textStart, err := readPclntab(f) + require.NoError(t, err) + + table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart)) + require.NoError(t, err) + + gosymNames := make(map[string]struct{}, len(table.Funcs)) + for _, fn := range table.Funcs { + gosymNames[fn.Name] = struct{}{} + } + + for _, sym := range syms { + if _, ok := gosymNames[sym.name]; !ok { + return true + } + } + return false +} + +func Test_packagePathFromSymbolName(t *testing.T) { + tests := []struct { + name string + expected string + }{ + {"path/filepath.IsLocal", "path/filepath"}, + // pointer-receiver method + {"golang.org/x/net/html.(*Tokenizer).Next", "golang.org/x/net/html"}, + // versioned (major-version-suffixed) module path + {"github.com/foo/bar/v2.Parse", "github.com/foo/bar/v2"}, + {"github.com/foo/bar/v2.(*Client).Do", "github.com/foo/bar/v2"}, + {"github.com/foo/bar.Parse.func1", "github.com/foo/bar"}, + {"main.main", "main"}, + {"runtime.gcBgMarkWorker", "runtime"}, + // generic instantiations: type arguments must not corrupt the package path + {"foo/bar.Do[net/url.Values]", "foo/bar"}, + {"github.com/foo/bar.Map[go.shape.int,go.shape.string]", "github.com/foo/bar"}, + {"main.Do[go.shape.int]", "main"}, + // no package-qualifying dot + {"runtime", ""}, + // compiler/linker-generated symbols belong to no package + {"type:.eq.[]string", ""}, + {"type..hash.runtime._type", ""}, + {"go:string.\"foo\"", ""}, + // pre-go1.20 toolchains generated symbols with "." where newer ones use ":" + {"go.buildid", ""}, + {"go.type.*runtime._type", ""}, + {"go.itab.*os.File,io.Reader", ""}, + {"go.string.\"foo\"", ""}, + // module paths that begin with "go." are not compiler-generated + {"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"}, + {"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, packagePathFromSymbolName(test.name)) + }) + } +} diff --git a/syft/pkg/golang.go b/syft/pkg/golang.go index 9656f83b1..1a119e8dc 100644 --- a/syft/pkg/golang.go +++ b/syft/pkg/golang.go @@ -22,6 +22,13 @@ type GolangBinaryBuildinfoEntry struct { // GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2"). GoExperiments []string `json:"goExperiments,omitempty" cyclonedx:"goExperiments"` + + // Symbols are the fully qualified function symbols from this module that are compiled into the binary + // (e.g., "github.com/foo/bar.(*Type).Method"), extracted from the binary symbol table (pclntab). + // Populated only when the golang cataloger's capture-symbols scope covers this package: the "all" scope + // populates every module package plus the synthetic stdlib package, while the "stdlib" scope populates + // only the stdlib package. + Symbols []string `json:"symbols,omitempty"` } // GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum diff --git a/syft/pkg/vcpkg.go b/syft/pkg/vcpkg.go index 165f846f1..23577c545 100644 --- a/syft/pkg/vcpkg.go +++ b/syft/pkg/vcpkg.go @@ -8,19 +8,36 @@ const ( Builtin VcpkgRegistryKind = "builtin" ) -// used for metadata. Includes summary of data found in manifest (vcpkg.json file) relevant to just that vcpkg +// VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package. type VcpkgManifest struct { - Description []string `json:"description,omitempty"` - Documentation string `json:"documentation,omitempty"` - FullVersion string `json:"full-version"` - Version string `json:"version"` - PortVersion int `json:"port-version"` - Maintainers []string `json:"maintainers,omitempty"` - Name string `json:"name"` - Supports string `json:"supports,omitempty"` - // to show where it came from + // Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest). + Description []string `json:"description,omitempty"` + + // Documentation is the URL to the package's documentation. + Documentation string `json:"documentation,omitempty"` + + // FullVersion is the complete version string including the port-version suffix (e.g. "1.2.3#2"). + FullVersion string `json:"full-version"` + + // Version is the upstream package version without the port-version suffix (e.g. "1.2.3"). + Version string `json:"version"` + + // PortVersion is the vcpkg-specific packaging revision for a given upstream version. + PortVersion int `json:"port-version"` + + // Maintainers are the people responsible for maintaining the vcpkg port. + Maintainers []string `json:"maintainers,omitempty"` + + // Name is the package name as declared in the manifest. + Name string `json:"name"` + + // Supports is the platform expression describing which triplets the package can be built for (e.g. "!windows"). + Supports string `json:"supports,omitempty"` + + // Registry indicates where the package definition came from. Registry *VcpkgRegistryEntry `json:"registry,omitempty"` - // found by looking at build folder to find target. ex. "x64-linux" + + // Triplet is the build target discovered from the build folder (e.g. "x64-linux"). Triplet string `json:"triplet,omitempty"` } From bd0917fe93e3e12bf02a22dd79b1a03f83d299f7 Mon Sep 17 00:00:00 2001 From: Synvoya Date: Tue, 14 Jul 2026 00:38:32 +1000 Subject: [PATCH 059/168] fix(javascript): strip peer-dep suffix from deno.lock npm keys (#5055) deno.lock npm keys append resolved peer dependencies after the version, separated by "_" (e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped "@scope/name@1.2.3_@scope+peer@4.5.6"). parseDenoNpmNameVersion split on the last "@", capturing a peer's "@" and producing a garbage package name plus the peer's version (and a corrupt PURL that breaks vulnerability matching). Strip the peer suffix (the first "_" after the version "@") before splitting name@version; a semver version never contains "_". Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com> Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com> --- .../cataloger/javascript/parse_deno_lock.go | 30 ++++++++++++++----- .../javascript/parse_deno_lock_test.go | 22 ++++++++++++++ .../javascript/test-fixtures/deno/deno.lock | 6 ++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock.go b/syft/pkg/cataloger/javascript/parse_deno_lock.go index c11bc98f7..4ad2b8cec 100644 --- a/syft/pkg/cataloger/javascript/parse_deno_lock.go +++ b/syft/pkg/cataloger/javascript/parse_deno_lock.go @@ -96,19 +96,35 @@ func parseDenoJsrNameVersion(nameVersion string) (name, version string) { } func parseDenoNpmNameVersion(nameVersion string) (name, version string) { + // deno.lock npm keys append resolved peer dependencies after the version, + // separated by "_", e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped + // "@scope/name@1.2.3_@scope+peer@4.5.6". The peer suffix must be stripped + // before splitting name@version, otherwise LastIndex("@") splits on a peer's + // "@" and yields a garbage name and the peer's version. + // + // A semver version never contains "_", and it begins immediately after the + // package name's version "@", so the first "_" following that "@" starts the + // peer list. + var versionAt int if strings.HasPrefix(nameVersion, "@") { - rest := nameVersion[1:] - idx := strings.LastIndex(rest, "@") + // scoped "@scope/name@version": the version "@" is the second "@" + idx := strings.Index(nameVersion[1:], "@") if idx <= 0 { return "", "" } - return nameVersion[:idx+1], rest[idx+1:] + versionAt = idx + 1 + } else { + versionAt = strings.Index(nameVersion, "@") + if versionAt <= 0 { + return "", "" + } } - idx := strings.LastIndex(nameVersion, "@") - if idx <= 0 { - return "", "" + + base := nameVersion + if underscore := strings.Index(nameVersion[versionAt:], "_"); underscore >= 0 { + base = nameVersion[:versionAt+underscore] } - return nameVersion[:idx], nameVersion[idx+1:] + return base[:versionAt], base[versionAt+1:] } func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package { diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go index 2e70bc32a..83fe9abb7 100644 --- a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go @@ -55,6 +55,28 @@ func TestParseDenoLock(t *testing.T) { Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", }, }, + { + // npm key carries a resolved peer dependency suffix ("_typescript@6.0.3") + Name: "typedoc", + Version: "0.28.19", + PURL: "pkg:npm/typedoc@0.28.19", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{ + Integrity: "sha512-TYPEDOCtestfixtureintegrityAAAA==", + }, + }, + { + // scoped npm key with a scope-encoded ("+") peer dependency suffix + Name: "@algolia/autocomplete-preset-algolia", + Version: "1.17.9", + PURL: "pkg:npm/%40algolia/autocomplete-preset-algolia@1.17.9", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{ + Integrity: "sha512-ALGOLIAtestfixtureintegrityBBBB==", + }, + }, } for i := range expectedPkgs { diff --git a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock index c6ecf9efd..365a18a8c 100644 --- a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock +++ b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock @@ -19,6 +19,12 @@ "npm": { "chalk@5.3.0": { "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + }, + "typedoc@0.28.19_typescript@6.0.3": { + "integrity": "sha512-TYPEDOCtestfixtureintegrityAAAA==" + }, + "@algolia/autocomplete-preset-algolia@1.17.9_@algolia+client-search@5.50.0": { + "integrity": "sha512-ALGOLIAtestfixtureintegrityBBBB==" } }, "remote": { From 7745e450fad93ba0986d5e1b06bea5ff7c2d26e7 Mon Sep 17 00:00:00 2001 From: Arpit Jain <3242828+arpitjain099@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:45:12 +0900 Subject: [PATCH 060/168] fix panic parsing a rockspec comment that ends at EOF (#5053) The hand-written rockspec parser reads a byte past the end of the buffer in two spots when a comment runs right up to the end of the file. In parseRockspecBlock, when a block starts with a leading comment that consumes the rest of the file, the SkipWhitespace afterward leaves the index at len(data) and the following `c = data[*i]` reads out of range. In parseComment, `data[*i]` is read after the index is advanced to check for a CR/LF pair, so a bare carriage return as the last byte reads past the end. Both cases show up with a rockspec whose final line is a comment ending in a lone \r with no trailing newline. That is malformed but harmless input, and the panic aborts the whole Lua cataloger, so every valid Lua package in the same scan gets dropped. Guard both reads with a length check and return cleanly at EOF. Added table cases covering a comment-only file and a trailing comment, both ending in a bare CR. Signed-off-by: arpitjain099 --- syft/pkg/cataloger/lua/rockspec_parser.go | 5 ++++- syft/pkg/cataloger/lua/rockspec_parser_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/lua/rockspec_parser.go b/syft/pkg/cataloger/lua/rockspec_parser.go index 56c9a87f2..be8630293 100644 --- a/syft/pkg/cataloger/lua/rockspec_parser.go +++ b/syft/pkg/cataloger/lua/rockspec_parser.go @@ -76,6 +76,9 @@ func parseRockspecBlock(data []byte, i *int, locals map[string]string) ([]rocksp if c == '-' { parseComment(data, i) parsing.SkipWhitespace(data, i) + if *i >= len(data) { + return out, nil + } c = data[*i] } @@ -424,7 +427,7 @@ func parseComment(data []byte, i *int) { // Rest of a line is a comment. Deals with CR, LF and CR/LF if c == '\n' { break - } else if c == '\r' && data[*i] == '\n' { + } else if c == '\r' && *i < len(data) && data[*i] == '\n' { *i++ break } diff --git a/syft/pkg/cataloger/lua/rockspec_parser_test.go b/syft/pkg/cataloger/lua/rockspec_parser_test.go index 0914d2695..aca545e00 100644 --- a/syft/pkg/cataloger/lua/rockspec_parser_test.go +++ b/syft/pkg/cataloger/lua/rockspec_parser_test.go @@ -185,6 +185,14 @@ end test = "blah" `, }, + { + name: "comment only ending in bare carriage return at EOF", + content: "--\r", + }, + { + name: "trailing comment ending in bare carriage return at EOF", + content: "foo = \"bar\"\n-- x\r", + }, { name: "invalid complex syntax", wantErr: require.Error, From 3cc3ecb003079b5a909ac1ff0e4c30d256997059 Mon Sep 17 00:00:00 2001 From: Synvoya Date: Tue, 14 Jul 2026 00:57:32 +1000 Subject: [PATCH 061/168] Fix panic on cabal.project.freeze 'installed' boot-lib constraints (#5040) GHC boot-lib constraints (any.base installed) have no ' ==', so fields[1] panicked with index out of range and the recover dropped all Haskell packages. Skip constraint lines without a version. Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com> Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com> --- syft/pkg/cataloger/haskell/parse_cabal_freeze.go | 4 ++++ syft/pkg/cataloger/haskell/testdata/cabal.project.freeze | 2 ++ 2 files changed, 6 insertions(+) diff --git a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go index 614524cae..b7f11093e 100644 --- a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go +++ b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go @@ -45,6 +45,10 @@ func parseCabalFreeze(_ context.Context, _ file.Resolver, _ *generic.Environment line = line[startPkgEncoding:endPkgEncoding] fields := strings.Split(line, " ==") + // skip constraints without a version (e.g. GHC boot libs: "any.base installed") + if len(fields) < 2 { + continue + } pkgName, pkgVersion := fields[0], fields[1] pkgs = append( diff --git a/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze b/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze index f20ce8cb5..6843f5d47 100644 --- a/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze +++ b/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze @@ -1,5 +1,7 @@ active-repositories: hackage.haskell.org:merge constraints: any.Cabal ==3.2.1.0, + any.array installed, + any.base installed, any.Diff ==0.4.1, any.HTTP ==4000.3.16, any.HUnit ==1.6.2.0, From da77ea388a01943e5108cb1eafbe4b5c21cd0155 Mon Sep 17 00:00:00 2001 From: Ali Asghar <98263017+alliasgher@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:15:01 +0500 Subject: [PATCH 062/168] docs: fix stale commands in binary cataloger README (#5047) Update the binary classifier cataloger developer README to match the current workflow: - Testing section used the non-existent flag -must-use-full-binaries against .../testdata/...; the real flag is -must-use-original-binaries and Test_Cataloger_PositiveCases lives in the binary package. Fix the command and add a single-fixture variant. - Point add-snippet references at the make target and note the (Y/n/q) prompt requires y to write the snippet. - Fix the test file name (classifier_cataloger_test.go) and document the go generate ./internal/capabilities step for new classifiers. Fixes #4510 Signed-off-by: alliasgher --- syft/pkg/cataloger/binary/README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/syft/pkg/cataloger/binary/README.md b/syft/pkg/cataloger/binary/README.md index f6b31290a..68de0723e 100644 --- a/syft/pkg/cataloger/binary/README.md +++ b/syft/pkg/cataloger/binary/README.md @@ -5,8 +5,9 @@ > 1. head to the correct directory: `cd testdata` > 2. add a new entry to `config.yaml` to track where to get the binary from (verify the entry with `make list`) > 3. run `make download` to get the binary -> 4. run `make add-snippet` and follow the prompts (use `/` to search) -> 5. add a new test case to `Test_Cataloger_PositiveCases` in `../cataloger_test.go` +> 4. run `make add-snippet` and follow the prompts (use `/` to search; answer `y` at the `(Y/n/q)` prompt to write the snippet) +> 5. add a new test case to `Test_Cataloger_PositiveCases` in `../classifier_cataloger_test.go` +> 6. if you added a new classifier, regenerate the capabilities catalog from the repo root and commit the result: `go generate ./internal/capabilities` To test the binary cataloger we run it against a set of files ("test fixtures"). There are two kinds of test fixtures: @@ -32,7 +33,7 @@ And use tooling to list and manage the fixtures: - `make list` - list all fixtures - `make download` - download binaries that are not covered by a snippet - `make download-all` - download all binaries -- `go run ./manager add-snippet` - add a new snippet based off of a configured binary +- `make add-snippet` - add a new snippet based off of a configured binary - `capture-snippet.sh` - add a new snippet based off of a binary on your local machine (not recommended, but allowed) There is a `config.yaml` that tracks all binaries that the tests can use. This makes it possible to download it at any time from a hosted source. Today the only method allowed is to download a container image and extract files out. @@ -74,10 +75,17 @@ from-images: ## Testing The test cases have been setup to allow testing against full binaries or a mix of both (default). -To force running only against full binaries run with: +To force running only against the original full binaries (instead of snippets), run with the +`-must-use-original-binaries` flag: ```bash -go test -must-use-full-binaries ./syft/pkg/cataloger/binary/testdata/... +go test -v ./syft/pkg/cataloger/binary -run Test_Cataloger_PositiveCases/ -must-use-original-binaries +``` + +To run against a single fixture, append its name to the `-run` filter: + +```bash +go test -v ./syft/pkg/cataloger/binary -run Test_Cataloger_PositiveCases/ -must-use-original-binaries ``` ## Adding a new test fixture @@ -91,7 +99,7 @@ Even if you are adding a snippet, it is best practice to: 1. Follow the steps above to [add a full binary](#adding-a-full-binary) -2. Run `go run ./manager add-snippet` and follow the prompts to create a new snippet +2. Run `make add-snippet` and follow the prompts to create a new snippet (answer `y` at the `(Y/n/q)` prompt to write the snippet) - you should see your binary in the list of binaries to choose from. If not, check step 2 - if the search results in no matching snippets, you can specify your own search with `--search-for ` - you should see a new snippet file created in `snippets/` From 76ede661db6d246424e8144f12323c3239945a1e Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 13 Jul 2026 15:10:12 -0400 Subject: [PATCH 063/168] fix: allow more PEP440-compliant characters in python versions (#4964) Signed-off-by: Keith Zantow --- .../cataloger/python/parse_requirements.go | 9 +- .../python/parse_requirements_test.go | 135 ++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/python/parse_requirements.go b/syft/pkg/cataloger/python/parse_requirements.go index 08d284c79..a7117bdee 100644 --- a/syft/pkg/cataloger/python/parse_requirements.go +++ b/syft/pkg/cataloger/python/parse_requirements.go @@ -29,8 +29,13 @@ const ( // namePattern matches: requests[security] namePattern = `(?P\w[\w\[\],\s-_\.]+)` - // versionConstraintPattern matches: == 2.8.* (including local version identifiers, e.g. == 1.2.3+gcr.2) - versionConstraintPattern = `(?P([^\S\r\n]*[~=>!<]+\s*[0-9a-zA-Z.*+]+[^\S\r\n]*,?)+)?(@[^\S\r\n]*(?P[^;]*))?` + // versionConstraintPattern matches: == 2.8.* + // the version token accepts every character PEP 440 versions can contain: digits and letters, + // '.' (release/segment separator), '!' (epoch), '+' (local version), '*' (wildcard), and the + // alternate segment separators '-' and '_'. See: + // https://packaging.python.org/en/latest/specifications/version-specifiers/#version-scheme + // 1!2.0 1.0rc1 1.0-alpha-1 1.0_beta_2 1.0.post1 1.0.dev1 1.2.3+gcr.2 2.8.* + versionConstraintPattern = `(?P([^\S\r\n]*[~=>!<]+\s*[0-9a-zA-Z.*+!_-]+[^\S\r\n]*,?)+)?(@[^\S\r\n]*(?P[^;]*))?` // markersPattern matches: python_version < "2.7" and sys_platform == "linux" markersPattern = `(;(?P.*))?` diff --git a/syft/pkg/cataloger/python/parse_requirements_test.go b/syft/pkg/cataloger/python/parse_requirements_test.go index 8c9d4e54c..735804e50 100644 --- a/syft/pkg/cataloger/python/parse_requirements_test.go +++ b/syft/pkg/cataloger/python/parse_requirements_test.go @@ -386,6 +386,111 @@ func Test_newRequirement(t *testing.T) { Markers: "sys_platform == 'linux'", }, }, + { + name: "epoch", + raw: "pkg == 1!2.0.0", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1!2.0.0", + }, + }, + { + name: "pre-release", + raw: "pkg == 1.0rc1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0rc1", + }, + }, + { + name: "pre-release with dash separators", + raw: "pkg == 1.0-alpha-1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0-alpha-1", + }, + }, + { + name: "pre-release with underscore separators", + raw: "pkg == 1.0_beta_2", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0_beta_2", + }, + }, + { + name: "post-release", + raw: "pkg == 1.0.post1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0.post1", + }, + }, + { + name: "implicit post-release", + raw: "pkg == 1.0-1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0-1", + }, + }, + { + name: "dev-release", + raw: "pkg == 1.0.dev1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0.dev1", + }, + }, + { + name: "local version with dash separator", + raw: "pkg == 1.0+ubuntu-1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0+ubuntu-1", + }, + }, + { + name: "local version with underscore separator", + raw: "pkg == 1.0+ubuntu_1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1.0+ubuntu_1", + }, + }, + { + name: "all segments combined", + raw: "pkg == 1!1.0a1.post2.dev3+local.1", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1!1.0a1.post2.dev3+local.1", + }, + }, + { + name: "release wildcard", + raw: "pkg == 2.*", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 2.*", + }, + }, + { + name: "epoch with markers and hashes", + raw: "pkg == 1!2.0 ; python_version < '3.8' --hash=sha256:abc123", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: "== 1!2.0", + Markers: "python_version < '3.8' --hash=sha256:abc123", + }, + }, + { + name: "compound constraint with exclusion is unaffected", + raw: "pkg >= 1.0.0, != 1.1.0, < 2.0.0", + want: &unprocessedRequirement{ + Name: "pkg", + VersionConstraint: ">= 1.0.0, != 1.1.0, < 2.0.0", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -427,6 +532,36 @@ func Test_parseVersion(t *testing.T) { version: " === 1.2.3+ubuntu1 ", want: "1.2.3+ubuntu1", }, + { + name: "pre-release", + version: "== 1.0rc1", + want: "1.0rc1", + }, + { + name: "pre-release with dash separators", + version: "== 1.0-alpha-1", + want: "1.0-alpha-1", + }, + { + name: "post-release", + version: "== 1.0.post1", + want: "1.0.post1", + }, + { + name: "dev-release", + version: "== 1.0.dev1", + want: "1.0.dev1", + }, + { + name: "local version with dash separator", + version: "== 1.0+ubuntu-1", + want: "1.0+ubuntu-1", + }, + { + name: "all segments combined", + version: "== 1.0a1.post2.dev3+local.1", + want: "1.0a1.post2.dev3+local.1", + }, { name: "resolve lowest, simple constraint", version: " >= 1.0.0 ", From 987ae7f26a3b29b5615d6de1be248d593468c19d Mon Sep 17 00:00:00 2001 From: Mike Zolotarov <96788373+activeobd@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:25:31 +0200 Subject: [PATCH 064/168] fix: PE case-insensitive extensions (Win32/ISO 9660 compatibility) (#4996) * fix: PE case-insensitive extensions (Win32/ISO 9660 compatibility) Signed-off-by: Alex Goodman * add tests Signed-off-by: Alex Goodman * expand cases to bpl files and surrounding catalogers Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/binary/capabilities.yaml | 3 +++ syft/pkg/cataloger/binary/pe_package_cataloger.go | 2 +- syft/pkg/cataloger/binary/pe_package_cataloger_test.go | 6 +++++- .../binary/testdata/glob-paths/src/winarchive.BPL | 1 + .../binary/testdata/glob-paths/src/winlibrary.DLL | 1 + .../binary/testdata/glob-paths/src/winprogram.EXE | 1 + syft/pkg/cataloger/dotnet/capabilities.yaml | 8 ++++++++ syft/pkg/cataloger/dotnet/cataloger_test.go | 8 ++++++++ syft/pkg/cataloger/dotnet/deps_binary_cataloger.go | 6 +++++- .../dotnet/testdata/glob-paths/src/winsomething.BPL | 1 + .../dotnet/testdata/glob-paths/src/winsomething.DLL | 1 + .../dotnet/testdata/glob-paths/src/winsomething.EXE | 1 + 12 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL create mode 100644 syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL create mode 100644 syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index a40db6e16..18e265ae0 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -1055,6 +1055,9 @@ catalogers: detector: # AUTO-GENERATED method: glob # AUTO-GENERATED criteria: # AUTO-GENERATED + - '**/*.DLL' + - '**/*.EXE' + - '**/*.BPL' - '**/*.dll' - '**/*.exe' - '**/*.bpl' diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger.go b/syft/pkg/cataloger/binary/pe_package_cataloger.go index 16292be76..74abb379d 100644 --- a/syft/pkg/cataloger/binary/pe_package_cataloger.go +++ b/syft/pkg/cataloger/binary/pe_package_cataloger.go @@ -16,7 +16,7 @@ import ( // BPL (Borland Package Library) files are PE-format binaries used by Delphi and C++Builder. func NewPEPackageCataloger() pkg.Cataloger { return generic.NewCataloger("pe-binary-package-cataloger"). - WithParserByGlobs(parsePE, "**/*.dll", "**/*.exe", "**/*.bpl") + WithParserByGlobs(parsePE, "**/*.DLL", "**/*.EXE", "**/*.BPL", "**/*.dll", "**/*.exe", "**/*.bpl") } func parsePE(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger_test.go b/syft/pkg/cataloger/binary/pe_package_cataloger_test.go index 7ba3eae3f..18b5c7d40 100644 --- a/syft/pkg/cataloger/binary/pe_package_cataloger_test.go +++ b/syft/pkg/cataloger/binary/pe_package_cataloger_test.go @@ -70,12 +70,16 @@ func Test_PEPackageCataloger_Globs(t *testing.T) { expected []string }{ { - name: "obtain PE binary files (dll, exe, bpl)", + name: "obtain PE binary files (dll, exe, bpl), including uppercase extensions", fixture: "testdata/glob-paths", expected: []string{ "src/library.dll", "src/program.exe", "src/archive.bpl", + // uppercase extensions appear on Windows/ISO 9660 filesystems and must also match + "src/winlibrary.DLL", + "src/winprogram.EXE", + "src/winarchive.BPL", }, }, } diff --git a/syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL new file mode 100644 index 000000000..159643c0f --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL @@ -0,0 +1 @@ +bogus PE contents diff --git a/syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL new file mode 100644 index 000000000..159643c0f --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL @@ -0,0 +1 @@ +bogus PE contents diff --git a/syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE new file mode 100644 index 000000000..159643c0f --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE @@ -0,0 +1 @@ +bogus PE contents diff --git a/syft/pkg/cataloger/dotnet/capabilities.yaml b/syft/pkg/cataloger/dotnet/capabilities.yaml index aa6b9c499..b6cf64d64 100644 --- a/syft/pkg/cataloger/dotnet/capabilities.yaml +++ b/syft/pkg/cataloger/dotnet/capabilities.yaml @@ -39,6 +39,10 @@ catalogers: - '**/*.deps.json' - '**/*.dll' - '**/*.exe' + - '**/*.bpl' + - '**/*.DLL' + - '**/*.EXE' + - '**/*.BPL' metadata_types: # AUTO-GENERATED - pkg.DotnetDepsEntry - pkg.DotnetPortableExecutableEntry @@ -173,6 +177,10 @@ catalogers: criteria: - '**/*.dll' - '**/*.exe' + - '**/*.bpl' + - '**/*.DLL' + - '**/*.EXE' + - '**/*.BPL' metadata_types: # AUTO-GENERATED - pkg.DotnetPortableExecutableEntry package_types: # AUTO-GENERATED diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go index 04b2caf20..dae8d9db9 100644 --- a/syft/pkg/cataloger/dotnet/cataloger_test.go +++ b/syft/pkg/cataloger/dotnet/cataloger_test.go @@ -36,6 +36,10 @@ func TestCataloger_Globs(t *testing.T) { "src/something.bpl", "src/something.dll", "src/something.exe", + // uppercase extensions appear on Windows/ISO 9660 filesystems and must also match + "src/winsomething.DLL", + "src/winsomething.EXE", + "src/winsomething.BPL", }, }, { @@ -47,6 +51,10 @@ func TestCataloger_Globs(t *testing.T) { "src/something.deps.json", "src/something.dll", "src/something.exe", + // uppercase extensions appear on Windows/ISO 9660 filesystems and must also match + "src/winsomething.DLL", + "src/winsomething.EXE", + "src/winsomething.BPL", }, // the binary cataloger probes executables by MIME type to find embedded bundles, // but the glob fixtures aren't real binaries so those queries go unfulfilled diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index ea96b1710..15f19de22 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -27,6 +27,10 @@ const ( dllGlob = "**/*.dll" exeGlob = "**/*.exe" bplGlob = "**/*.bpl" + // uppercase variants match PE files on case-preserving Windows/ISO 9660 filesystems (doublestar globs are case-sensitive) + dllGlobUpper = "**/*.DLL" + exeGlobUpper = "**/*.EXE" + bplGlobUpper = "**/*.BPL" ) var elfMagic = []byte{0x7f, 'E', 'L', 'F'} @@ -483,7 +487,7 @@ func readDepsJSON(resolver file.Resolver, loc file.Location) (*depsJSON, error) // findPEFiles locates and parses all PE files (dll/exe). func findPEFiles(resolver file.Resolver) ([]logicalPE, error, error) { - peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob, bplGlob) + peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob, bplGlob, dllGlobUpper, exeGlobUpper, bplGlobUpper) if err != nil { return nil, nil, fmt.Errorf("unable to find PE files: %w", err) } diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL new file mode 100644 index 000000000..159643c0f --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL @@ -0,0 +1 @@ +bogus PE contents diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL new file mode 100644 index 000000000..159643c0f --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL @@ -0,0 +1 @@ +bogus PE contents diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE new file mode 100644 index 000000000..159643c0f --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE @@ -0,0 +1 @@ +bogus PE contents From e854078fb02b7704f5630818662144d12096dac2 Mon Sep 17 00:00:00 2001 From: M_C_J Date: Mon, 13 Jul 2026 20:29:30 +0100 Subject: [PATCH 065/168] fix: use printf instead of echo to fix ANSI color output (#4978) Signed-off-by: Jouini Mohamed Chaker --- install.sh | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index f4921c255..bf9222d06 100755 --- a/install.sh +++ b/install.sh @@ -35,7 +35,8 @@ is_command() ( ) echo_stderr() ( - echo "$@" 1>&2 + IFS=" " + printf "%b\n" "$*" 1>&2 ) _logp=2 @@ -75,12 +76,12 @@ init_colors log_tag() ( case $1 in - 0) echo "${RED}${BOLD}[error]${RESET}" ;; - 1) echo "${RED}[warn]${RESET}" ;; - 2) echo "[info]${RESET}" ;; - 3) echo "${BLUE}[debug]${RESET}" ;; - 4) echo "${PURPLE}[trace]${RESET}" ;; - *) echo "[$1]" ;; + 0) printf "%b" "${RED}${BOLD}[error]${RESET}" ;; + 1) printf "%b" "${RED}[warn]${RESET}" ;; + 2) printf "%b" "[info]${RESET}" ;; + 3) printf "%b" "${BLUE}[debug]${RESET}" ;; + 4) printf "%b" "${PURPLE}[trace]${RESET}" ;; + *) printf "[%s]" "$1" ;; esac ) From fabd7609146848a8489e1d7bd5308246227cd638 Mon Sep 17 00:00:00 2001 From: Jonas Boos Date: Mon, 13 Jul 2026 21:31:35 +0200 Subject: [PATCH 066/168] fix: correct Spring LDAP group ID in purl generation (#4908) * fix: correct Spring LDAP group ID in purl generation The Spring LDAP artifacts use the group ID org.springframework.ldap (not org.springframework). This caused syft to generate incorrect purls like pkg:maven/spring-ldap-core/spring-ldap-core@3.1.4 instead of the correct pkg:maven/org.springframework.ldap/spring-ldap-core@3.1.4. Add all Spring LDAP artifacts to the DefaultArtifactIDToGroupID map: - spring-ldap - spring-ldap-core - spring-ldap-core-tiger - spring-ldap-ldif-core - spring-ldap-odm - spring-ldap-test Closes #4030 Signed-off-by: Jonas Boos Signed-off-by: Alex Goodman * fix formatting Signed-off-by: Alex Goodman * add test Signed-off-by: Alex Goodman --------- Signed-off-by: Jonas Boos Signed-off-by: Alex Goodman Co-authored-by: Jonas Boos Co-authored-by: Alex Goodman --- .../internal/cpegenerate/java_groupid_map.go | 9 ++++++++- syft/pkg/cataloger/java/package_url_test.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go index f607e8316..52c8ad250 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go +++ b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go @@ -381,7 +381,14 @@ var DefaultArtifactIDToGroupID = map[string]string{ "spring-jms": "org.springframework", "spring-jmx": "org.springframework", "spring-jpa": "org.springframework", - "spring-ldap": "org.springframework", + "spring-ldap": "org.springframework.ldap", + "spring-ldap-core": "org.springframework.ldap", + "spring-ldap-core-tiger": "org.springframework.ldap", + "spring-ldap-ldif-batch": "org.springframework.ldap", + "spring-ldap-ldif-core": "org.springframework.ldap", + "spring-ldap-odm": "org.springframework.ldap", + "spring-ldap-test": "org.springframework.ldap", + "spring-ldap-tiger": "org.springframework.ldap", "spring-messaging": "org.springframework", "spring-mock": "org.springframework", "spring-ojb": "org.springframework", diff --git a/syft/pkg/cataloger/java/package_url_test.go b/syft/pkg/cataloger/java/package_url_test.go index 1661f3fdb..0e0532ffc 100644 --- a/syft/pkg/cataloger/java/package_url_test.go +++ b/syft/pkg/cataloger/java/package_url_test.go @@ -171,6 +171,20 @@ func Test_groupIDFromJavaMetadata(t *testing.T) { metadata: pkg.JavaArchive{}, expect: "org.apache.ant", }, + { + // regression for github.com/anchore/syft/issues/4030: spring-ldap artifacts + // live under org.springframework.ldap, not org.springframework + name: "known package list spring-ldap-core", + pkgName: "spring-ldap-core", + metadata: pkg.JavaArchive{}, + expect: "org.springframework.ldap", + }, + { + name: "known package list spring-ldap", + pkgName: "spring-ldap", + metadata: pkg.JavaArchive{}, + expect: "org.springframework.ldap", + }, { name: "java manifest", metadata: pkg.JavaArchive{ From 4312699dd6f3d0d97e69a3a3fa6a0f2465239974 Mon Sep 17 00:00:00 2001 From: ChrisJr404 Date: Mon, 13 Jul 2026 15:33:51 -0400 Subject: [PATCH 067/168] cyclonedx: import firmware-typed components as packages (#4855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CycloneDX decoder was filtering on a hard-coded set of component types (application/framework/library/machine-learning-model). Anything else — including "firmware" — fell through silently, so a BOM describing u-boot or other firmware ended up with no packages and grype reported no matches (anchore/grype#2537). @kzantow confirmed in the issue thread that firmware should be included and gave this section as the spot to update. Add ComponentTypeFirmware to the case list and a regression test that constructs a single-firmware-component BOM and asserts the decoded SBOM contains the package. Closes anchore/grype#2537 Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com> Co-authored-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com> --- .../internal/cyclonedxutil/helpers/decoder.go | 2 +- .../cyclonedxutil/helpers/decoder_test.go | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/syft/format/internal/cyclonedxutil/helpers/decoder.go b/syft/format/internal/cyclonedxutil/helpers/decoder.go index 0f1783362..aff76f66a 100644 --- a/syft/format/internal/cyclonedxutil/helpers/decoder.go +++ b/syft/format/internal/cyclonedxutil/helpers/decoder.go @@ -62,7 +62,7 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str switch component.Type { case cyclonedx.ComponentTypeOS: case cyclonedx.ComponentTypeContainer: - case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel: + case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel, cyclonedx.ComponentTypeFirmware: p := decodeComponent(component) idMap[component.BOMRef] = p if component.BOMRef != "" { diff --git a/syft/format/internal/cyclonedxutil/helpers/decoder_test.go b/syft/format/internal/cyclonedxutil/helpers/decoder_test.go index b5c1f3c27..5092c3ff0 100644 --- a/syft/format/internal/cyclonedxutil/helpers/decoder_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/decoder_test.go @@ -259,6 +259,30 @@ func Test_decode(t *testing.T) { } } +func Test_decode_includesFirmwareComponents(t *testing.T) { + // CycloneDX BOMs that describe firmware (e.g. u-boot) use + // component type "firmware". Prior to issue #2537 the decoder skipped + // these and downstream tools (e.g. grype) reported zero matches. + bom := cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + { + BOMRef: "u-boot", + Type: cyclonedx.ComponentTypeFirmware, + Name: "u-boot", + Version: "2024.04", + PackageURL: "pkg:generic/u-boot@2024.04", + }, + }, + } + model, err := ToSyftModel(&bom) + require.NoError(t, err) + + pkgs := model.Artifacts.Packages.Sorted() + require.Len(t, pkgs, 1, "firmware component should be imported as a package") + assert.Equal(t, "u-boot", pkgs[0].Name) + assert.Equal(t, "2024.04", pkgs[0].Version) +} + func Test_relationshipDirection(t *testing.T) { cyclonedx_bom := cyclonedx.BOM{Metadata: nil, Components: &[]cyclonedx.Component{ From f6b5d3e736b837f71155620ed6495432cd5a020e Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:40:16 -0400 Subject: [PATCH 068/168] perf: group golang symbols by package path to reduce the sbom size (#5064) --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- internal/constants.go | 2 +- schema/json/schema-16.1.9.json | 11 ++- schema/json/schema-latest.json | 11 ++- syft/pkg/cataloger/golang/package.go | 2 +- syft/pkg/cataloger/golang/parse_go_binary.go | 51 +++++++---- .../cataloger/golang/parse_go_binary_test.go | 69 ++++++++++++--- syft/pkg/cataloger/golang/stdlib_package.go | 2 +- .../cataloger/golang/stdlib_package_test.go | 4 +- syft/pkg/cataloger/golang/symbols.go | 67 ++++++++++----- syft/pkg/cataloger/golang/symbols_test.go | 85 ++++++++++++++----- syft/pkg/golang.go | 16 ++-- 11 files changed, 235 insertions(+), 85 deletions(-) diff --git a/internal/constants.go b/internal/constants.go index b21c5adda..f1b50a324 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -15,5 +15,5 @@ const ( // 16.1.6 - add Dependencies to ElixirMixLockEntry metadata // 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger // 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support - // 16.1.9 - add Symbols to GolangBinaryBuildinfoEntry metadata + // 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata ) diff --git a/schema/json/schema-16.1.9.json b/schema/json/schema-16.1.9.json index e1363bf94..75eea2f94 100644 --- a/schema/json/schema-16.1.9.json +++ b/schema/json/schema-16.1.9.json @@ -1655,11 +1655,14 @@ "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." }, "symbols": { - "items": { - "type": "string" + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array", - "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package." + "type": "object", + "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package." } }, "type": "object", diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index e1363bf94..75eea2f94 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1655,11 +1655,14 @@ "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." }, "symbols": { - "items": { - "type": "string" + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array", - "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package." + "type": "object", + "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package." } }, "type": "object", diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go index 4e1e39a80..a08202ab3 100644 --- a/syft/pkg/cataloger/golang/package.go +++ b/syft/pkg/cataloger/golang/package.go @@ -45,7 +45,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi return p } -func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments, symbols []string) pkg.GolangBinaryBuildinfoEntry { +func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments []string, symbols map[string][]string) pkg.GolangBinaryBuildinfoEntry { if dep.Replace != nil { dep = dep.Replace } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 8553ec4be..a4d24c918 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -54,9 +54,10 @@ type goBinaryCataloger struct { symbolScope cataloging.SymbolScope // stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the - // binary's location), populated during parsing and consumed by stdlibProcessor when it builds the - // synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run concurrently. - stdlibSymbols map[file.Coordinates][]string + // binary's location), grouped by import path, populated during parsing and consumed by stdlibProcessor + // when it builds the synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run + // concurrently. + stdlibSymbols map[file.Coordinates]map[string][]string stdlibSymbolsMu sync.Mutex } @@ -65,29 +66,49 @@ func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger { licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts), mainModuleVersion: opts.MainModuleVersion, symbolScope: opts.CaptureSymbols, - stdlibSymbols: make(map[file.Coordinates][]string), + stdlibSymbols: make(map[file.Coordinates]map[string][]string), } } -// recordStdlibSymbols merges the standard-library symbols discovered for a binary location so the -// stdlib processor can attach them to the synthetic stdlib package. -func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols []string) { +// recordStdlibSymbols merges the standard-library symbols discovered for a binary location (grouped by +// import path) so the stdlib processor can attach them to the synthetic stdlib package. +func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols map[string][]string) { if len(symbols) == 0 { return } c.stdlibSymbolsMu.Lock() defer c.stdlibSymbolsMu.Unlock() - merged := slices.Concat(c.stdlibSymbols[coord], symbols) - slices.Sort(merged) - c.stdlibSymbols[coord] = slices.Compact(merged) + existing := c.stdlibSymbols[coord] + if existing == nil { + existing = make(map[string][]string) + c.stdlibSymbols[coord] = existing + } + for path, names := range symbols { + merged := slices.Concat(existing[path], names) + slices.Sort(merged) + existing[path] = slices.Compact(merged) + } } -// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a copy -// so callers cannot alias (and later mutate or race on) the map's internal slice. -func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string { +// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a deep +// copy so callers cannot alias (and later mutate or race on) the map's internal state. +func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) map[string][]string { c.stdlibSymbolsMu.Lock() defer c.stdlibSymbolsMu.Unlock() - return slices.Clone(c.stdlibSymbols[coord]) + return cloneSymbolGroups(c.stdlibSymbols[coord]) +} + +// cloneSymbolGroups returns a deep copy of a symbol group map (import path -> local symbol names), or nil +// when the input is empty. +func cloneSymbolGroups(groups map[string][]string) map[string][]string { + if len(groups) == 0 { + return nil + } + out := make(map[string][]string, len(groups)) + for path, names := range groups { + out[path] = slices.Clone(names) + } + return out } // parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler. @@ -221,7 +242,7 @@ func missingMainModule(mod *extendedBuildInfo) bool { return mod.Main == moduleFromPartialPackageBuild } -func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols []string) pkg.Package { +func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols map[string][]string) pkg.Package { gbs := getBuildSettings(mod.Settings) lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version) gover, experiments := getExperimentsFromVersion(mod.GoVersion) diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index 648068822..9881f91cc 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -1427,12 +1427,14 @@ func (alwaysErrorReader) Read(_ []byte) (int, error) { func Test_buildGoPkgInfo_symbolScope(t *testing.T) { location := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"}) - // the symbols a binary would carry once scanFile has extracted them: one main-package symbol, one - // dependency symbol, and one standard-library symbol. For the "none" scope scanFile never runs, so the - // build info carries no symbols at all. + // the symbols a binary would carry once scanFile has extracted them: one main-package symbol, two + // dependency symbols from distinct packages within the same module (so the per-package keying of the + // emitted map is visible), and one standard-library symbol. For the "none" scope scanFile never runs, + // so the build info carries no symbols at all. populatedSymbols := []binarySymbol{ {packagePath: "main", name: "main.main"}, {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, + {packagePath: "github.com/foo/bar/baz", name: "github.com/foo/bar/baz.Helper"}, {packagePath: "net/http", name: "net/http.(*Client).Do"}, } @@ -1440,9 +1442,9 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) { name string scope cataloging.SymbolScope symbols []binarySymbol - wantMainSyms []string - wantDepSyms []string - wantStdlibSyms []string + wantMainSyms map[string][]string + wantDepSyms map[string][]string + wantStdlibSyms map[string][]string }{ { name: "none captures nothing", @@ -1453,15 +1455,18 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) { name: "stdlib captures only the stdlib package", scope: cataloging.SymbolScopeStdlib, symbols: populatedSymbols, - wantStdlibSyms: []string{"net/http.(*Client).Do"}, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, }, { - name: "all captures module and stdlib packages", - scope: cataloging.SymbolScopeAll, - symbols: populatedSymbols, - wantMainSyms: []string{"main.main"}, - wantDepSyms: []string{"github.com/foo/bar.Parse"}, - wantStdlibSyms: []string{"net/http.(*Client).Do"}, + name: "all captures module and stdlib packages", + scope: cataloging.SymbolScopeAll, + symbols: populatedSymbols, + wantMainSyms: map[string][]string{"main": {"main"}}, + wantDepSyms: map[string][]string{ + "github.com/foo/bar": {"Parse"}, + "github.com/foo/bar/baz": {"Helper"}, + }, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, }, } @@ -1491,3 +1496,41 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) { }) } } + +// Test_recordStdlibSymbols_merge covers the merge path where the same binary location records stdlib +// symbols more than once. This happens in production for universal/fat Mach-O binaries: scanFile yields +// one build info per architecture and each is recorded under the same location coordinates. +func Test_recordStdlibSymbols_merge(t *testing.T) { + coord := file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"} + c := newGoBinaryCataloger(CatalogerConfig{}) + + c.recordStdlibSymbols(coord, map[string][]string{ + "net/http": {"(*Client).Do", "Get"}, + "net/url": {"Parse"}, + }) + // a second architecture: overlapping names (must dedup), a new name for an existing package, and a new package + c.recordStdlibSymbols(coord, map[string][]string{ + "net/http": {"Get", "Post"}, + "os": {"Open"}, + }) + + assert.Equal(t, map[string][]string{ + "net/http": {"(*Client).Do", "Get", "Post"}, + "net/url": {"Parse"}, + "os": {"Open"}, + }, c.stdlibSymbolsFor(coord)) +} + +// Test_stdlibSymbolsFor_isolation asserts the returned map is a deep copy: mutating it (or its slices) +// must not corrupt the cataloger's guarded internal state. +func Test_stdlibSymbolsFor_isolation(t *testing.T) { + coord := file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"} + c := newGoBinaryCataloger(CatalogerConfig{}) + c.recordStdlibSymbols(coord, map[string][]string{"net/http": {"Get"}}) + + got := c.stdlibSymbolsFor(coord) + got["net/http"] = append(got["net/http"], "MUTATED") + got["net/url"] = []string{"Parse"} + + assert.Equal(t, map[string][]string{"net/http": {"Get"}}, c.stdlibSymbolsFor(coord), "internal state must be unaffected by caller mutation") +} diff --git a/syft/pkg/cataloger/golang/stdlib_package.go b/syft/pkg/cataloger/golang/stdlib_package.go index 1743372e5..d310898f1 100644 --- a/syft/pkg/cataloger/golang/stdlib_package.go +++ b/syft/pkg/cataloger/golang/stdlib_package.go @@ -50,7 +50,7 @@ func (c *goBinaryCataloger) stdlibPackageAndRelationships(ctx context.Context, p return goCompilerPkgs, relationships } -func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols []string) *pkg.Package { +func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols map[string][]string) *pkg.Package { stdlibCpe, err := generateStdlibCpe(version) if err != nil { return nil diff --git a/syft/pkg/cataloger/golang/stdlib_package_test.go b/syft/pkg/cataloger/golang/stdlib_package_test.go index 3c4e8ff75..b0f49dae4 100644 --- a/syft/pkg/cataloger/golang/stdlib_package_test.go +++ b/syft/pkg/cataloger/golang/stdlib_package_test.go @@ -88,7 +88,7 @@ func Test_stdlibPackageAndRelationships(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)} + c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates]map[string][]string)} gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, tt.pkgs) assert.Len(t, gotPkgs, tt.wantPkgs) assert.Len(t, gotRels, tt.wantRels) @@ -138,7 +138,7 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) { Type: artifact.DependencyOfRelationship, } - c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)} + c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates]map[string][]string)} gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, []pkg.Package{p}) require.Len(t, gotPkgs, 1) diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go index 3d553a739..1d8067dff 100644 --- a/syft/pkg/cataloger/golang/symbols.go +++ b/syft/pkg/cataloger/golang/symbols.go @@ -267,12 +267,15 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) { } // moduleSymbols attributes each extracted symbol to the module that owns it (by longest module path prefix -// of the symbol's package path) and returns a sorted, deduplicated list of symbol names per module path. -// Symbols from the "main" package are attributed to the main module. Standard-library symbols (which belong -// to no module) are collected separately and returned as the second value so they can be attached to the +// of the symbol's package path) and returns, per module path, the symbols grouped by the import path of the +// owning package. Each inner value is a sorted, deduplicated list of symbol names local to that package +// (the import path prefix stripped, e.g. "github.com/foo/bar.(*T).M" under key "github.com/foo/bar" becomes +// "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main" +// import path the linker assigns. Standard-library symbols (which belong to no module) are collected +// separately and returned as the second value, grouped by import path, so they can be attached to the // synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a // recognizable stdlib import path are dropped. -func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string][]string, stdlib []string) { +func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) { if len(symbols) == 0 { return nil, nil } @@ -287,41 +290,67 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod } } - results := make(map[string][]string) + results := make(map[string]map[string][]string) + stdlib = make(map[string][]string) for _, sym := range symbols { - pkgPath := sym.packagePath - if pkgPath == mainPackage && main != nil { - // the linker renames the main package's import path to "main" - pkgPath = main.Path + importPath := sym.packagePath + + // the linker renames the main package's import path to "main"; attribute it to the main module, + // but keep "main" as the group key since the original import path is not recoverable. + attrPath := importPath + if importPath == mainPackage && main != nil { + attrPath = main.Path } var best string for _, modPath := range modulePaths { - if len(modPath) > len(best) && (pkgPath == modPath || strings.HasPrefix(pkgPath, modPath+"/")) { + if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) { best = modPath } } + + local := localSymbolName(sym.name, importPath) if best == "" { - if pkgPath != mainPackage && isStandardImportPath(pkgPath) { - stdlib = append(stdlib, sym.name) + if importPath != mainPackage && isStandardImportPath(importPath) { + stdlib[importPath] = append(stdlib[importPath], local) } continue } - results[best] = append(results[best], sym.name) + if results[best] == nil { + results[best] = make(map[string][]string) + } + results[best][importPath] = append(results[best][importPath], local) } - for modPath, names := range results { - slices.Sort(names) - results[modPath] = slices.Compact(names) + for _, byImport := range results { + sortCompactGroups(byImport) } - if len(stdlib) > 0 { - slices.Sort(stdlib) - stdlib = slices.Compact(stdlib) + sortCompactGroups(stdlib) + if len(stdlib) == 0 { + stdlib = nil } return results, stdlib } +// localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g. +// "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned +// unchanged when it does not carry the expected prefix. +func localSymbolName(name, importPath string) string { + if importPath != "" && strings.HasPrefix(name, importPath+".") { + return name[len(importPath)+1:] + } + return name +} + +// sortCompactGroups sorts and deduplicates each symbol list in a group keyed by import path, in place. +func sortCompactGroups(groups map[string][]string) { + for path, names := range groups { + slices.Sort(names) + groups[path] = slices.Compact(names) + } +} + // isStandardImportPath reports whether path is a Go standard-library import path. This mirrors the rule // the Go toolchain uses: a path is standard if the element before its first slash contains no dot (e.g. // "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go index 703c3210a..72ca613da 100644 --- a/syft/pkg/cataloger/golang/symbols_test.go +++ b/syft/pkg/cataloger/golang/symbols_test.go @@ -22,8 +22,8 @@ func Test_moduleSymbols(t *testing.T) { tests := []struct { name string symbols []binarySymbol - expected map[string][]string - expectedStdlib []string + expected map[string]map[string][]string + expectedStdlib map[string][]string }{ { name: "no symbols", @@ -31,47 +31,47 @@ func Test_moduleSymbols(t *testing.T) { expected: nil, }, { - name: "attribute symbols by longest module path prefix", + name: "attribute symbols by longest module path prefix, grouped by import path", symbols: []binarySymbol{ {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, {packagePath: "github.com/foo/bar/internal/util", name: "github.com/foo/bar/internal/util.(*Helper).Do"}, {packagePath: "github.com/foo/bar/v2", name: "github.com/foo/bar/v2.Parse"}, }, - expected: map[string][]string{ + expected: map[string]map[string][]string{ "github.com/foo/bar": { - "github.com/foo/bar.Parse", - "github.com/foo/bar/internal/util.(*Helper).Do", + "github.com/foo/bar": {"Parse"}, + "github.com/foo/bar/internal/util": {"(*Helper).Do"}, }, "github.com/foo/bar/v2": { - "github.com/foo/bar/v2.Parse", + "github.com/foo/bar/v2": {"Parse"}, }, }, }, { - name: "main package symbols are attributed to the main module", + name: "main package symbols are attributed to the main module and keyed by the main import path", symbols: []binarySymbol{ {packagePath: "main", name: "main.main"}, {packagePath: "github.com/someorg/somecli/cmd", name: "github.com/someorg/somecli/cmd.Execute"}, }, - expected: map[string][]string{ + expected: map[string]map[string][]string{ "github.com/someorg/somecli": { - "github.com/someorg/somecli/cmd.Execute", - "main.main", + "github.com/someorg/somecli/cmd": {"Execute"}, + "main": {"main"}, }, }, }, { - name: "stdlib and runtime symbols are collected separately", + name: "stdlib and runtime symbols are collected separately, grouped by import path", symbols: []binarySymbol{ {packagePath: "runtime", name: "runtime.main"}, {packagePath: "net/http", name: "net/http.(*Client).Do"}, {packagePath: "internal/abi", name: "internal/abi.(*Type).Kind"}, }, - expected: map[string][]string{}, - expectedStdlib: []string{ - "internal/abi.(*Type).Kind", - "net/http.(*Client).Do", - "runtime.main", + expected: map[string]map[string][]string{}, + expectedStdlib: map[string][]string{ + "internal/abi": {"(*Type).Kind"}, + "net/http": {"(*Client).Do"}, + "runtime": {"main"}, }, }, { @@ -80,9 +80,9 @@ func Test_moduleSymbols(t *testing.T) { {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, }, - expected: map[string][]string{ + expected: map[string]map[string][]string{ "github.com/foo/bar": { - "github.com/foo/bar.Parse", + "github.com/foo/bar": {"Parse"}, }, }, }, @@ -97,6 +97,53 @@ func Test_moduleSymbols(t *testing.T) { } } +func Test_localSymbolName(t *testing.T) { + tests := []struct { + name string + symbol string + importPath string + expected string + }{ + { + name: "function", + symbol: "github.com/foo/bar.Parse", + importPath: "github.com/foo/bar", + expected: "Parse", + }, + { + name: "pointer-receiver method", + symbol: "github.com/foo/bar.(*T).M", + importPath: "github.com/foo/bar", + expected: "(*T).M", + }, + { + // type-argument brackets sit to the right of the stripped prefix and must be preserved + name: "generic instantiation", + symbol: "github.com/foo/bar.Do[net/url.Values]", + importPath: "github.com/foo/bar", + expected: "Do[net/url.Values]", + }, + { + // a sibling package must not be stripped by a shorter import path (the "." boundary guards this) + name: "prefix mismatch is returned unchanged", + symbol: "github.com/foo/bar/sub.Func", + importPath: "github.com/foo/bar", + expected: "github.com/foo/bar/sub.Func", + }, + { + name: "empty import path is returned unchanged", + symbol: "runtime.main", + importPath: "", + expected: "runtime.main", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, localSymbolName(tt.symbol, tt.importPath)) + }) + } +} + func Test_getSymbols(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("PE binaries are not supported for symbol extraction") diff --git a/syft/pkg/golang.go b/syft/pkg/golang.go index 1a119e8dc..8b133a8de 100644 --- a/syft/pkg/golang.go +++ b/syft/pkg/golang.go @@ -23,12 +23,16 @@ type GolangBinaryBuildinfoEntry struct { // GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2"). GoExperiments []string `json:"goExperiments,omitempty" cyclonedx:"goExperiments"` - // Symbols are the fully qualified function symbols from this module that are compiled into the binary - // (e.g., "github.com/foo/bar.(*Type).Method"), extracted from the binary symbol table (pclntab). - // Populated only when the golang cataloger's capture-symbols scope covers this package: the "all" scope - // populates every module package plus the synthetic stdlib package, while the "stdlib" scope populates - // only the stdlib package. - Symbols []string `json:"symbols,omitempty"` + // Symbols are the function symbols from this module that are compiled into the binary, extracted from + // the binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each + // value is the sorted, deduplicated list of symbol names local to that package, i.e. with the import + // path prefix stripped (e.g. import path "github.com/foo/bar" -> "(*Type).Method"). The fully qualified + // name is the import path, a ".", and the local name. One exception: the binary's main package appears + // under the key "main" (the name the linker assigns), not its original source import path, which is not + // recoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers + // this package: the "all" scope populates every module package plus the synthetic stdlib package, while + // the "stdlib" scope populates only the stdlib package. + Symbols map[string][]string `json:"symbols,omitempty"` } // GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum From 552ddbfadb488c6f0e73fa418671a855845f768e Mon Sep 17 00:00:00 2001 From: Monibee-Fudgekins <148405685+Monibee-Fudgekins@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:43:34 -0400 Subject: [PATCH 069/168] fix: map NuGet PURLs to dotnet package type (#4861) * test: use unique NuGet PURL subtest name Signed-off-by: Monibee Fudgekins * fix: emit canonical NuGet PURL type Signed-off-by: Monibee Fudgekins * chore: regenerate dotnet capabilities for nuget PURL type regenerated after emitting canonical nuget PURL type for dotnet packages. Signed-off-by: Alex Goodman --------- Signed-off-by: Monibee Fudgekins Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/dotnet/capabilities.yaml | 8 ++++---- syft/pkg/type.go | 2 +- syft/pkg/type_test.go | 11 ++++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/syft/pkg/cataloger/dotnet/capabilities.yaml b/syft/pkg/cataloger/dotnet/capabilities.yaml index b6cf64d64..2d1107ec2 100644 --- a/syft/pkg/cataloger/dotnet/capabilities.yaml +++ b/syft/pkg/cataloger/dotnet/capabilities.yaml @@ -50,8 +50,8 @@ catalogers: - dotnet - npm purl_types: # AUTO-GENERATED - - dotnet - npm + - nuget json_schema_types: # AUTO-GENERATED - DotnetDepsEntry - DotnetPortableExecutableEntry @@ -91,7 +91,7 @@ catalogers: package_types: # AUTO-GENERATED - dotnet purl_types: # AUTO-GENERATED - - dotnet + - nuget json_schema_types: # AUTO-GENERATED - DotnetDepsEntry capabilities: # MANUAL - edit capabilities here @@ -137,7 +137,7 @@ catalogers: package_types: # AUTO-GENERATED - dotnet purl_types: # AUTO-GENERATED - - dotnet + - nuget json_schema_types: # AUTO-GENERATED - DotnetPackagesLockEntry capabilities: # MANUAL - preserved across regeneration @@ -186,7 +186,7 @@ catalogers: package_types: # AUTO-GENERATED - dotnet purl_types: # AUTO-GENERATED - - dotnet + - nuget json_schema_types: # AUTO-GENERATED - DotnetPortableExecutableEntry capabilities: # MANUAL - edit capabilities here diff --git a/syft/pkg/type.go b/syft/pkg/type.go index b4d42853f..c354c949c 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -128,7 +128,7 @@ func (t Type) PackageURLType() string { case DebPkg: return "deb" case DotnetPkg: - return "dotnet" + return packageurl.TypeNuget case ErlangOTPPkg: return packageurl.TypeOTP case GemPkg: diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 514d06b13..4c4fd7add 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -3,6 +3,7 @@ package pkg import ( "testing" + "github.com/anchore/packageurl-go" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" ) @@ -166,7 +167,11 @@ func TestTypeFromPURL(t *testing.T) { expectedTypes.Remove(string(VcpkgPkg)) for _, test := range tests { - t.Run(string(test.expected), func(t *testing.T) { + name := test.name + if name == "" { + name = string(test.expected) + } + t.Run(name, func(t *testing.T) { actual := TypeFromPURL(test.purl) if actual != "" { @@ -179,3 +184,7 @@ func TestTypeFromPURL(t *testing.T) { assert.ElementsMatch(t, expectedTypes.List(), pkgTypes.List(), "missing one or more package types to test against (maybe a package type was added?)") } + +func TestPackageURLType(t *testing.T) { + assert.Equal(t, packageurl.TypeNuget, DotnetPkg.PackageURLType()) +} From 2805655ab04603446a5422905d32cb953b560556 Mon Sep 17 00:00:00 2001 From: ChrisJr404 Date: Tue, 14 Jul 2026 08:49:53 -0400 Subject: [PATCH 070/168] feat(cmd): log total scan duration at info level (#4858) Closes #4587 Adds a single info-level log line at the end of every `syft scan` run that reports the total scan wall-clock time, e.g.: [0003] INFO scan completed in 2.017s Per maintainer guidance on the issue, the line is logged at INFO so it surfaces with `-v` (alongside the existing per-cataloger timing) and stays out of the default TUI / stdout. Useful for users who leave long scans running and want the overall time at a glance, without having to wrap the invocation in `time`. Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com> Co-authored-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com> --- cmd/syft/internal/commands/scan.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/syft/internal/commands/scan.go b/cmd/syft/internal/commands/scan.go index d9147dace..7db17fb7d 100644 --- a/cmd/syft/internal/commands/scan.go +++ b/cmd/syft/internal/commands/scan.go @@ -7,6 +7,7 @@ import ( "os" "reflect" "strings" + "time" "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" @@ -171,6 +172,11 @@ func validateArgs(cmd *cobra.Command, args []string, err string) error { } func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error { + scanStart := time.Now() + defer func() { + log.Infof("scan completed in %s", time.Since(scanStart).Round(time.Millisecond)) + }() + writer, err := opts.SBOMWriter() if err != nil { return err From ad828d659fdb5b29766662821ebd1392e258f118 Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Wed, 15 Jul 2026 15:29:03 -0400 Subject: [PATCH 071/168] fix: conan libxml2 CPE (#5016) Signed-off-by: Rez Moss --- .../internal/cpegenerate/candidate_by_package_type.go | 5 +++++ .../internal/cpegenerate/candidate_by_package_type_test.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go index 1b363c656..32c2131a1 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go +++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go @@ -508,6 +508,11 @@ var defaultCandidateAdditions = buildCandidateLookup( candidateKey{PkgName: "poco"}, candidateAddition{AdditionalVendors: []string{"pocoproject"}}, }, + { + pkg.ConanPkg, + candidateKey{PkgName: "libxml2"}, + candidateAddition{AdditionalVendors: []string{"xmlsoft"}}, + }, }) var defaultCandidateRemovals = buildCandidateRemovalLookup( diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go index c610167f8..4d903b14c 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go @@ -232,3 +232,8 @@ func Test_findProductsToRemove(t *testing.T) { }) } } + +func Test_defaultCandidateAdditions_conan_libxml2(t *testing.T) { + vendors := findAdditionalVendors(defaultCandidateAdditions, pkg.ConanPkg, "libxml2", "") + assert.Contains(t, vendors, "xmlsoft") +} From ed499fd2d0e23529bb3213cfe883be8068342ce4 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:52:46 -0400 Subject: [PATCH 072/168] chore: migrate `.goreleaser` config to use docker_v2 (#4608) --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .github/workflows/release.yaml | 9 +- .github/workflows/validations.yaml | 10 +- .goreleaser.yaml | 370 ++++++----------------------- Dockerfile | 22 +- Dockerfile.debug | 26 +- Dockerfile.debug-nonroot | 12 + Dockerfile.nonroot | 22 +- Taskfile.yaml | 10 +- 8 files changed, 121 insertions(+), 360 deletions(-) create mode 100644 Dockerfile.debug-nonroot diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9d3981475..39e4fe7da 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -63,8 +63,13 @@ jobs: # spot disabled: reliability for build workflows (used for releases too) # goreleaser uses parallelism of 12, so we need more CPUs # s3-cache: faster actions cache - # tmpfs: faster io-intensive workflows - runs-on: runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache+tmpfs + # volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch + # image + binary build and OOMs (runs out of disk); size it well above peak usage. + # provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate + # the tmpfs IO speed on disk, for ~1c/run. + # note: tmpfs intentionally omitted -- a RAM-backed dist/ never touches the enlarged + # volume and competes with the build's memory peak, so keep dist/ on disk. + runs-on: runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache/volume=120gb:gp3:500mbs:4000iops permissions: contents: write # required for creating the GitHub release and pushing the version tag packages: write # required for publishing release artifacts to GitHub packages diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index c520a05b7..746319f30 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -92,10 +92,16 @@ jobs: # runs-on.com: compute instances for parallel builds # spot disabled: reliability for build workflows (used for releases too) # goreleaser uses parallelism of 12, so we need more CPUs - # tmpfs: faster io-intensive workflows + # tmpfs intentionally omitted -- a RAM-backed dist/ competes with the goreleaser + # build's memory peak (parallel binaries + multi-arch image assembly) and OOMs the + # instance; keep dist/ on disk. # note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the # shared cache backend that the trusted release workflow reads from (cache poisoning). - runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=tmpfs" + # volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch + # image + binary build and OOMs (runs out of disk); size it well above peak usage. + # provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate + # the tmpfs IO speed on disk, for ~1c/run. + runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/volume=120gb:gp3:500mbs:4000iops" permissions: contents: read steps: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3edce4f6d..d5bb7de18 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -73,306 +73,88 @@ brews: description: *description license: "Apache License 2.0" -dockers: - # production images... - - image_templates: - - anchore/syft:{{.Tag}}-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-amd64 - goarch: amd64 +dockers_v2: + # production images (scratch base, root) + - id: production dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" + ids: &docker-ids + - linux-build + images: &docker-images + - anchore/syft + - ghcr.io/anchore/syft + platforms: &docker-platforms + - linux/amd64 + - linux/arm64 + - linux/ppc64le + - linux/riscv64 + - linux/s390x + labels: &docker-labels + "org.opencontainers.image.created": "{{.Date}}" + "org.opencontainers.image.title": "syft" + "org.opencontainers.image.description": "CLI tool and library for generating a Software Bill of Materials from container images and filesystems" + "org.opencontainers.image.source": "{{.GitURL}}" + "org.opencontainers.image.revision": "{{.FullCommit}}" + "org.opencontainers.image.vendor": "Anchore, Inc." + "org.opencontainers.image.version": "{{.Version}}" + "org.opencontainers.image.licenses": "Apache-2.0" + "io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/anchore/syft/main/README.md" + "io.artifacthub.package.logo-url": "https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" + "io.artifacthub.package.license": "Apache-2.0" + tags: + - latest + - "{{.Tag}}" + # DEBIAN_VERSION 13 (trixie) is the first distroless release with a riscv64 base image + # and is a superset of debian 12 for the other targeted arches; docker_v2 builds all + # platforms in a single buildx invocation, so build_args cannot vary per platform. + build_args: &docker-build-args + DEBIAN_VERSION: "13" + # disable provenance attestations to keep the manifest free of unknown/unknown entries + flags: &docker-flags + - "--provenance=false" + # SBOMs are produced separately for the archives (see the sboms section) + sbom: &docker-sbom "false" - - image_templates: - - anchore/syft:{{.Tag}}-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-arm64v8 - goarch: arm64 - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-ppc64le - goarch: ppc64le - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/ppc64le" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-riscv64 - goarch: riscv64 - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/riscv64" - - "--build-arg=DEBIAN_VERSION=13" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-s390x - - ghcr.io/anchore/syft:{{.Tag}}-s390x - goarch: s390x - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - # nonroot images... - - image_templates: - - anchore/syft:{{.Tag}}-nonroot-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64 - goarch: amd64 + # nonroot images + - id: nonroot dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" + ids: *docker-ids + images: *docker-images + platforms: *docker-platforms + labels: *docker-labels + tags: + - nonroot + - "{{.Tag}}-nonroot" + build_args: *docker-build-args + flags: *docker-flags + sbom: *docker-sbom - - image_templates: - - anchore/syft:{{.Tag}}-nonroot-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8 - goarch: arm64 - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-nonroot-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le - goarch: ppc64le - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/ppc64le" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-nonroot-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64 - goarch: riscv64 - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/riscv64" - - "--build-arg=DEBIAN_VERSION=13" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-nonroot-s390x - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x - goarch: s390x - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - # debug images... - - image_templates: - - anchore/syft:{{.Tag}}-debug-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-debug-amd64 - goarch: amd64 + # debug images (root) + - id: debug dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" + ids: *docker-ids + images: *docker-images + platforms: *docker-platforms + labels: *docker-labels + tags: + - debug + - "{{.Tag}}-debug" + build_args: *docker-build-args + flags: *docker-flags + sbom: *docker-sbom - - image_templates: - - anchore/syft:{{.Tag}}-debug-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8 - goarch: arm64 - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-debug-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le - goarch: ppc64le - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/ppc64le" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-debug-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64 - goarch: riscv64 - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/riscv64" - - "--build-arg=DEBIAN_VERSION=13" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/syft:{{.Tag}}-debug-s390x - - ghcr.io/anchore/syft:{{.Tag}}-debug-s390x - goarch: s390x - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - -docker_manifests: - - name_template: anchore/syft:latest - image_templates: - - anchore/syft:{{.Tag}}-amd64 - - anchore/syft:{{.Tag}}-arm64v8 - - anchore/syft:{{.Tag}}-ppc64le - - anchore/syft:{{.Tag}}-riscv64 - - anchore/syft:{{.Tag}}-s390x - - - name_template: ghcr.io/anchore/syft:latest - image_templates: - - ghcr.io/anchore/syft:{{.Tag}}-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-s390x - - - name_template: anchore/syft:{{.Tag}} - image_templates: - - anchore/syft:{{.Tag}}-amd64 - - anchore/syft:{{.Tag}}-arm64v8 - - anchore/syft:{{.Tag}}-ppc64le - - anchore/syft:{{.Tag}}-riscv64 - - anchore/syft:{{.Tag}}-s390x - - - name_template: ghcr.io/anchore/syft:{{.Tag}} - image_templates: - - ghcr.io/anchore/syft:{{.Tag}}-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-s390x - - # nonroot images... - - name_template: anchore/syft:nonroot - image_templates: - - anchore/syft:{{.Tag}}-nonroot-amd64 - - anchore/syft:{{.Tag}}-nonroot-arm64v8 - - anchore/syft:{{.Tag}}-nonroot-ppc64le - - anchore/syft:{{.Tag}}-nonroot-riscv64 - - anchore/syft:{{.Tag}}-nonroot-s390x - - - name_template: ghcr.io/anchore/syft:nonroot - image_templates: - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x - - - name_template: anchore/syft:{{.Tag}}-nonroot - image_templates: - - anchore/syft:{{.Tag}}-nonroot-amd64 - - anchore/syft:{{.Tag}}-nonroot-arm64v8 - - anchore/syft:{{.Tag}}-nonroot-ppc64le - - anchore/syft:{{.Tag}}-nonroot-riscv64 - - anchore/syft:{{.Tag}}-nonroot-s390x - - - name_template: ghcr.io/anchore/syft:{{.Tag}}-nonroot - image_templates: - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x - - # debug images... - - name_template: anchore/syft:debug - image_templates: - - anchore/syft:{{.Tag}}-debug-amd64 - - anchore/syft:{{.Tag}}-debug-arm64v8 - - anchore/syft:{{.Tag}}-debug-ppc64le - - anchore/syft:{{.Tag}}-debug-riscv64 - - anchore/syft:{{.Tag}}-debug-s390x - - - name_template: ghcr.io/anchore/syft:debug - image_templates: - - ghcr.io/anchore/syft:{{.Tag}}-debug-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-debug-s390x - - - name_template: anchore/syft:{{.Tag}}-debug - image_templates: - - anchore/syft:{{.Tag}}-debug-amd64 - - anchore/syft:{{.Tag}}-debug-arm64v8 - - anchore/syft:{{.Tag}}-debug-ppc64le - - anchore/syft:{{.Tag}}-debug-riscv64 - - anchore/syft:{{.Tag}}-debug-s390x - - - name_template: ghcr.io/anchore/syft:{{.Tag}}-debug - image_templates: - - ghcr.io/anchore/syft:{{.Tag}}-debug-amd64 - - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8 - - ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le - - ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64 - - ghcr.io/anchore/syft:{{.Tag}}-debug-s390x + # debug-nonroot images + - id: debug-nonroot + dockerfile: Dockerfile.debug-nonroot + ids: *docker-ids + images: *docker-images + platforms: *docker-platforms + labels: *docker-labels + tags: + - debug-nonroot + - "{{.Tag}}-debug-nonroot" + build_args: *docker-build-args + flags: *docker-flags + sbom: *docker-sbom sboms: - artifacts: archive diff --git a/Dockerfile b/Dockerfile index 86ff23db2..4dc169ab5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG DEBIAN_VERSION=12 +ARG DEBIAN_VERSION=13 FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:latest AS build FROM scratch @@ -8,23 +8,7 @@ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifica # create the /tmp dir, which is needed for image content cache WORKDIR /tmp -COPY syft / - -ARG BUILD_DATE -ARG BUILD_VERSION -ARG VCS_REF -ARG VCS_URL - -LABEL org.opencontainers.image.created=$BUILD_DATE -LABEL org.opencontainers.image.title="syft" -LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems" -LABEL org.opencontainers.image.source=$VCS_URL -LABEL org.opencontainers.image.revision=$VCS_REF -LABEL org.opencontainers.image.vendor="Anchore, Inc." -LABEL org.opencontainers.image.version=$BUILD_VERSION -LABEL org.opencontainers.image.licenses="Apache-2.0" -LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md" -LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" -LABEL io.artifacthub.package.license="Apache-2.0" +ARG TARGETPLATFORM +COPY ${TARGETPLATFORM}/syft / ENTRYPOINT ["/syft"] diff --git a/Dockerfile.debug b/Dockerfile.debug index 936b5ffc1..697527b10 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -1,28 +1,10 @@ -ARG DEBIAN_VERSION=12 -FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot +ARG DEBIAN_VERSION=13 +FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug # create the /tmp dir, which is needed for image content cache WORKDIR /tmp -COPY syft / - -USER nonroot - -ARG BUILD_DATE -ARG BUILD_VERSION -ARG VCS_REF -ARG VCS_URL - -LABEL org.opencontainers.image.created=$BUILD_DATE -LABEL org.opencontainers.image.title="syft" -LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems" -LABEL org.opencontainers.image.source=$VCS_URL -LABEL org.opencontainers.image.revision=$VCS_REF -LABEL org.opencontainers.image.vendor="Anchore, Inc." -LABEL org.opencontainers.image.version=$BUILD_VERSION -LABEL org.opencontainers.image.licenses="Apache-2.0" -LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md" -LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" -LABEL io.artifacthub.package.license="Apache-2.0" +ARG TARGETPLATFORM +COPY ${TARGETPLATFORM}/syft / ENTRYPOINT ["/syft"] diff --git a/Dockerfile.debug-nonroot b/Dockerfile.debug-nonroot new file mode 100644 index 000000000..0e152cc12 --- /dev/null +++ b/Dockerfile.debug-nonroot @@ -0,0 +1,12 @@ +ARG DEBIAN_VERSION=13 +FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot + +# create the /tmp dir, which is needed for image content cache +WORKDIR /tmp + +ARG TARGETPLATFORM +COPY ${TARGETPLATFORM}/syft / + +USER nonroot + +ENTRYPOINT ["/syft"] diff --git a/Dockerfile.nonroot b/Dockerfile.nonroot index c6789d4c7..61c937c70 100644 --- a/Dockerfile.nonroot +++ b/Dockerfile.nonroot @@ -1,28 +1,12 @@ -ARG DEBIAN_VERSION=12 +ARG DEBIAN_VERSION=13 FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:nonroot # create the /tmp dir, which is needed for image content cache WORKDIR /tmp -COPY syft / +ARG TARGETPLATFORM +COPY ${TARGETPLATFORM}/syft / USER nonroot -ARG BUILD_DATE -ARG BUILD_VERSION -ARG VCS_REF -ARG VCS_URL - -LABEL org.opencontainers.image.created=$BUILD_DATE -LABEL org.opencontainers.image.title="syft" -LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems" -LABEL org.opencontainers.image.source=$VCS_URL -LABEL org.opencontainers.image.revision=$VCS_REF -LABEL org.opencontainers.image.vendor="Anchore, Inc." -LABEL org.opencontainers.image.version=$BUILD_VERSION -LABEL org.opencontainers.image.licenses="Apache-2.0" -LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md" -LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" -LABEL io.artifacthub.package.license="Apache-2.0" - ENTRYPOINT ["/syft"] diff --git a/Taskfile.yaml b/Taskfile.yaml index d0ae4f20d..7e671c38e 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -39,6 +39,12 @@ vars: PROJECT_ROOT: sh: echo $PWD + # docker platform suffix goreleaser appends to snapshot image tags (e.g. latest-amd64). + # snapshot builds load per-arch images rather than a multi-arch manifest, so the smoke + # test must run the host-native tag to avoid emulation (and red-local/green-CI splits). + DOCKER_ARCH: + sh: go env GOARCH + # note: the snapshot dir must be a relative path starting with ./ # e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name SNAPSHOT_DIR: ./snapshot @@ -129,8 +135,8 @@ tasks: silent: true - "{{ .SNAPSHOT_BIN }} version" - "{{ .SNAPSHOT_BIN }} scan alpine:latest" - - docker run --rm anchore/syft:latest version - - docker run --rm anchore/syft:latest scan alpine:latest + - docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} version + - docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} scan alpine:latest ## Test-fixture-related targets ########################################### From aff2c247dbebf948534a510315cf4ae9a70299dd Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:14:03 -0400 Subject: [PATCH 073/168] feat: add safe tensor model type to SBOM output (#4844) --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- internal/constants.go | 3 +- .../packagemetadata/discover_type_names.go | 1 + internal/packagemetadata/generated.go | 1 + internal/packagemetadata/names.go | 1 + internal/task/package_tasks.go | 1 + schema/json/schema-16.1.10.json | 4575 +++++++++++++++++ schema/json/schema-latest.json | 54 +- syft/file/mock_resolver.go | 16 + .../helpers/originator_supplier_test.go | 1 + .../internal/spdxutil/helpers/source_info.go | 2 +- .../fileresolver/container_image_model.go | 20 +- .../container_image_model_test.go | 6 +- syft/pkg/cataloger/ai/capabilities.yaml | 43 + syft/pkg/cataloger/ai/cataloger.go | 30 +- syft/pkg/cataloger/ai/gguf_processor.go | 56 + syft/pkg/cataloger/ai/huggingface.go | 93 + syft/pkg/cataloger/ai/identity_dir.go | 93 + syft/pkg/cataloger/ai/identity_oci.go | 173 + syft/pkg/cataloger/ai/merge.go | 139 + syft/pkg/cataloger/ai/merge_test.go | 166 + syft/pkg/cataloger/ai/naming.go | 22 + syft/pkg/cataloger/ai/package.go | 14 + syft/pkg/cataloger/ai/parse_safetensors.go | 202 + .../cataloger/ai/parse_safetensors_model.go | 48 + .../pkg/cataloger/ai/parse_safetensors_oci.go | 117 + .../cataloger/ai/parse_safetensors_test.go | 1202 +++++ syft/pkg/cataloger/ai/processor.go | 191 +- .../ai/testdata/safetensors/README.md | 28 + .../ai/testdata/safetensors/extract_header.go | 149 + .../nomic-embed-475M.header.safetensors | Bin 0 -> 15520 bytes .../cataloger/golang/stdlib_package_test.go | 4 +- syft/pkg/safetensors.go | 48 + syft/pkg/type_test.go | 3 +- .../source/ocimodelsource/oci_model_source.go | 131 +- syft/source/ocimodelsource/registry_client.go | 118 +- .../registry_client_integration_test.go | 112 + .../registry_client_safetensors_test.go | 55 + 37 files changed, 7829 insertions(+), 89 deletions(-) create mode 100644 schema/json/schema-16.1.10.json create mode 100644 syft/pkg/cataloger/ai/gguf_processor.go create mode 100644 syft/pkg/cataloger/ai/huggingface.go create mode 100644 syft/pkg/cataloger/ai/identity_dir.go create mode 100644 syft/pkg/cataloger/ai/identity_oci.go create mode 100644 syft/pkg/cataloger/ai/merge.go create mode 100644 syft/pkg/cataloger/ai/merge_test.go create mode 100644 syft/pkg/cataloger/ai/naming.go create mode 100644 syft/pkg/cataloger/ai/parse_safetensors.go create mode 100644 syft/pkg/cataloger/ai/parse_safetensors_model.go create mode 100644 syft/pkg/cataloger/ai/parse_safetensors_oci.go create mode 100644 syft/pkg/cataloger/ai/parse_safetensors_test.go create mode 100644 syft/pkg/cataloger/ai/testdata/safetensors/README.md create mode 100644 syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go create mode 100644 syft/pkg/cataloger/ai/testdata/safetensors/nomic-embed-475M.header.safetensors create mode 100644 syft/pkg/safetensors.go create mode 100644 syft/source/ocimodelsource/registry_client_integration_test.go create mode 100644 syft/source/ocimodelsource/registry_client_safetensors_test.go diff --git a/internal/constants.go b/internal/constants.go index f1b50a324..4aa1849d3 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,7 +3,7 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.1.9" + JSONSchemaVersion = "16.1.10" // Changelog // 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). @@ -16,4 +16,5 @@ const ( // 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger // 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support // 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata + // 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger ) diff --git a/internal/packagemetadata/discover_type_names.go b/internal/packagemetadata/discover_type_names.go index 704c98c0b..cdcefa184 100644 --- a/internal/packagemetadata/discover_type_names.go +++ b/internal/packagemetadata/discover_type_names.go @@ -27,6 +27,7 @@ var knownNonMetadataTypeNames = strset.New( var knownMetadataTypeNames = strset.New( "DotnetPortableExecutableEntry", "GGUFFileHeader", + "SafeTensorsModelInfo", ) func DiscoverTypeNames() ([]string, error) { diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go index 78fe51347..65fc659c7 100644 --- a/internal/packagemetadata/generated.go +++ b/internal/packagemetadata/generated.go @@ -68,6 +68,7 @@ func AllTypes() []any { pkg.RubyGemspec{}, pkg.RustBinaryAuditEntry{}, pkg.RustCargoLockEntry{}, + pkg.SafeTensorsModelInfo{}, pkg.SnapEntry{}, pkg.SwiftPackageManagerResolvedEntry{}, pkg.SwiplPackEntry{}, diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go index e4370a897..98e5d47ae 100644 --- a/internal/packagemetadata/names.go +++ b/internal/packagemetadata/names.go @@ -130,6 +130,7 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"), jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"), jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"), + jsonNames(pkg.SafeTensorsModelInfo{}, "safetensors-model-info"), jsonNames(pkg.VcpkgManifest{}, "vcpkg-manifest"), ) diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index fa6e3a950..89b5bd201 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -191,6 +191,7 @@ func DefaultPackageTaskFactories() Factories { newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"), newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"), newSimplePackageTaskFactory(ai.NewGGUFCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "gguf", "ml"), + newSimplePackageTaskFactory(ai.NewSafeTensorsCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "safetensors", "ml"), // deprecated catalogers //////////////////////////////////////// // these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible) diff --git a/schema/json/schema-16.1.10.json b/schema/json/schema-16.1.10.json new file mode 100644 index 000000000..87756c843 --- /dev/null +++ b/schema/json/schema-16.1.10.json @@ -0,0 +1,4575 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.1.10/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object", + "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman." + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "AppleAppBundleEntry": { + "properties": { + "bundleIdentifier": { + "type": "string", + "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")" + }, + "name": { + "type": "string", + "description": "Name is the short name of the bundle" + }, + "displayName": { + "type": "string", + "description": "DisplayName is the user-facing name of the bundle" + }, + "executable": { + "type": "string", + "description": "Executable is the name of the executable within the bundle" + }, + "shortVersion": { + "type": "string", + "description": "ShortVersion is the release (marketing) version of the bundle" + }, + "version": { + "type": "string", + "description": "Version is the build version of the bundle, which often differs from the short version" + }, + "packageType": { + "type": "string", + "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)" + }, + "supportedPlatforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")" + }, + "minimumSystemVersion": { + "type": "string", + "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle" + }, + "minimumOSVersion": { + "type": "string", + "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)" + }, + "copyright": { + "type": "string", + "description": "Copyright is the human-readable copyright notice for the bundle" + }, + "platformName": { + "type": "string", + "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")" + }, + "sdkName": { + "type": "string", + "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")" + } + }, + "type": "object", + "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string", + "description": "Value is the CPE string identifier." + }, + "source": { + "type": "string", + "description": "Source is the source where this CPE was obtained or generated from." + } + }, + "type": "object", + "required": [ + "cpe" + ], + "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases." + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "DenoLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies is the list of package specifiers that this package depends on" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies" + ], + "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file" + }, + "DenoRemoteLockEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the remote URL from which the module fetcef" + }, + "integrity": { + "type": "string", + "description": "Integrity is the crpto hash of the package content for verification" + } + }, + "type": "object", + "required": [ + "url", + "integrity" + ], + "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file" + }, + "Descriptor": { + "properties": { + "name": { + "type": "string", + "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")." + }, + "version": { + "type": "string", + "description": "Version is the version of the tool that generated this SBOM." + }, + "configuration": { + "description": "Configuration contains the tool configuration used during SBOM generation." + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation." + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array", + "description": "Artifacts is the list of packages discovered and placed into the catalog" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array", + "description": "note: must have omitempty" + }, + "source": { + "$ref": "#/$defs/Source", + "description": "Source represents the original object that was cataloged" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease", + "description": "Distro represents the Linux distribution that was detected from the source" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor", + "description": "Descriptor is a block containing self-describing information about syft" + }, + "schema": { + "$ref": "#/$defs/Schema", + "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "type": { + "type": "string", + "description": "Type is type of entry could be package or project for internal refs" + }, + "executables": { + "additionalProperties": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing." + }, + "appCpe": { + "type": "string", + "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages." + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for this file within the SBOM." + }, + "location": { + "$ref": "#/$defs/Coordinates", + "description": "Location is the file path and layer information where this file was found." + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry", + "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type." + }, + "contents": { + "type": "string", + "description": "Contents is the file contents for small files." + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains cryptographic hashes of the file contents." + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array", + "description": "Licenses contains license information discovered within this file." + }, + "executable": { + "$ref": "#/$defs/Executable", + "description": "Executable contains executable metadata if this file is a binary." + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Unknowns contains unknown fields for forward compatibility." + } + }, + "type": "object", + "required": [ + "id", + "location" + ], + "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages." + }, + "FileLicense": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or text as found in the file." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence", + "description": "Evidence contains supporting evidence for this license detection." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ], + "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression." + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer", + "description": "Confidence is the confidence score for this license detection (0-100)." + }, + "offset": { + "type": "integer", + "description": "Offset is the byte offset where the license text starts in the file." + }, + "extent": { + "type": "integer", + "description": "Extent is the length of the license text in bytes." + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ], + "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level." + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer", + "description": "Mode is the Unix file permission mode in octal format." + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")." + }, + "linkDestination": { + "type": "string", + "description": "LinkDestination is the target path for symbolic links." + }, + "userID": { + "type": "integer", + "description": "UserID is the file owner user ID." + }, + "groupID": { + "type": "integer", + "description": "GroupID is the file owner group ID." + }, + "mimeType": { + "type": "string", + "description": "MIMEType is the MIME type of the file contents." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ], + "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file." + }, + "GgufFileHeader": { + "properties": { + "ggufVersion": { + "type": "integer", + "description": "GGUFVersion is the GGUF format version (e.g., 3)" + }, + "fileSize": { + "type": "integer", + "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")" + }, + "quantization": { + "type": "string", + "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")" + }, + "parameters": { + "type": "integer", + "description": "Parameters is the number of model parameters (if present in header)" + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensors in the model" + }, + "header": { + "type": "object", + "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication." + }, + "metadataHash": { + "type": "string", + "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames." + }, + "parts": { + "items": { + "$ref": "#/$defs/GgufFileHeader" + }, + "type": "array", + "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)." + } + }, + "type": "object", + "required": [ + "ggufVersion", + "tensorCount" + ], + "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file." + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + }, + "symbols": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object", + "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field." + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptBunLockEntry": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of runtime dependencies and their version specifiers" + }, + "optionalDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "OptionalDependencies is a map of optional dependencies and their version specifiers" + }, + "peerDependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "PeerDependencies is a map of peer dependencies and their version specifiers" + }, + "bin": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Bin is a map of binary names to the paths they are installed to" + }, + "os": { + "type": "string", + "description": "OS is the operating system constraint for the package (e.g. \"darwin\")" + }, + "cpu": { + "type": "string", + "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")" + } + }, + "type": "object", + "required": [ + "integrity", + "dependencies", + "optionalDependencies", + "peerDependencies", + "bin", + "os", + "cpu" + ], + "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\"" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptPnpmLockEntry": { + "properties": { + "resolution": { + "$ref": "#/$defs/PnpmLockResolution", + "description": "Resolution is the resolution information for the package" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolution", + "dependencies" + ], + "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies is a map of dependencies and their versions" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity", + "dependencies" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string", + "description": "Value is the raw license identifier or expression as found." + }, + "spdxExpression": { + "type": "string", + "description": "SPDXExpression is the parsed SPDX license expression." + }, + "type": { + "type": "string", + "description": "Type is the license type classification (e.g., declared, concluded, discovered)." + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URLs are URLs where license text or information can be found." + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array", + "description": "Locations are file locations where this license was discovered." + }, + "contents": { + "type": "string", + "description": "Contents is the full license text content." + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ], + "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations." + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string", + "description": "PrettyName is a human-readable operating system name with version." + }, + "name": { + "type": "string", + "description": "Name is the operating system name without version information." + }, + "id": { + "type": "string", + "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")." + }, + "idLike": { + "$ref": "#/$defs/IDLikes", + "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from." + }, + "version": { + "type": "string", + "description": "Version is the operating system version including codename if available." + }, + "versionID": { + "type": "string", + "description": "VersionID is the operating system version number or identifier." + }, + "versionCodename": { + "type": "string", + "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")." + }, + "buildID": { + "type": "string", + "description": "BuildID is a build identifier for the operating system." + }, + "imageID": { + "type": "string", + "description": "ImageID is an identifier for container or cloud images." + }, + "imageVersion": { + "type": "string", + "description": "ImageVersion is the version for container or cloud images." + }, + "variant": { + "type": "string", + "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")." + }, + "variantID": { + "type": "string", + "description": "VariantID is the lower-case operating system variant identifier." + }, + "homeURL": { + "type": "string", + "description": "HomeURL is the homepage URL for the operating system." + }, + "supportURL": { + "type": "string", + "description": "SupportURL is the support or help URL for the operating system." + }, + "bugReportURL": { + "type": "string", + "description": "BugReportURL is the bug reporting URL for the operating system." + }, + "privacyPolicyURL": { + "type": "string", + "description": "PrivacyPolicyURL is the privacy policy URL for the operating system." + }, + "cpeName": { + "type": "string", + "description": "CPEName is the Common Platform Enumeration name for the operating system." + }, + "supportEnd": { + "type": "string", + "description": "SupportEnd is the end of support date or version identifier." + }, + "extendedSupport": { + "type": "boolean", + "description": "ExtendedSupport indicates whether extended security or support is available." + } + }, + "type": "object", + "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files." + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Arbitrary key-value pairs that can be used to annotate a location" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/AppleAppBundleEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DenoLockEntry" + }, + { + "$ref": "#/$defs/DenoRemoteLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GgufFileHeader" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptBunLockEntry" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptPnpmLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SafetensorsModelInfo" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/VcpkgManifest" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PnpmLockResolution": { + "properties": { + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "integrity" + ], + "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmFileEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the file download URL" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest is the hash digest of the file hosted at the URL" + } + }, + "type": "object", + "required": [ + "url", + "digest" + ] + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests (for the base package without extras)" + }, + "marker": { + "type": "string", + "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment" + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications for the base package (without extras)" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPdmLockExtraVariant" + }, + "type": "array", + "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)" + } + }, + "type": "object", + "required": [ + "summary", + "files" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPdmLockExtraVariant": { + "properties": { + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependencies specific to this extras variant" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonPdmFileEntry" + }, + "type": "array", + "description": "Files are the package files specific to this variant (only populated if different from base)" + }, + "marker": { + "type": "string", + "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")" + } + }, + "type": "object", + "required": [ + "extras" + ], + "description": "PythonPdmLockExtraVariant represents a specific extras combination variant within a PDM lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string", + "description": "Parent is the ID of the parent artifact in this relationship." + }, + "child": { + "type": "string", + "description": "Child is the ID of the child artifact in this relationship." + }, + "type": { + "type": "string", + "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")." + }, + "metadata": { + "description": "Metadata contains additional relationship-specific metadata." + } + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ], + "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package." + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "SafetensorsModelInfo": { + "properties": { + "format": { + "type": "string", + "description": "Format is the source format label (always \"safetensors\" for this metadata type).\nPresent because the Docker AI model config blob carries an explicit format field" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (e.g., \"LlamaForCausalLM\",\n\"Qwen3MoeForConditionalGeneration\"). It is not present in the SafeTensors\nheader itself; it is enriched from the companion config.json\n\"architectures\" array when one is found alongside the model." + }, + "quantization": { + "type": "string", + "description": "Quantization describes tensor precision (e.g., \"BF16\", \"F16\", \"F32\", \"INT8\")." + }, + "parameters": { + "type": "integer", + "description": "Parameters is the total number of model parameters, computed from the tensor\nshapes in the SafeTensors header(s). For a sharded model it is the sum across\nevery shard." + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensor entries in the file header." + }, + "totalSize": { + "type": "string", + "description": "TotalSize is the total byte size of tensor data across all shards when known\n(from the Docker AI model config \"size\" field)." + }, + "shardCount": { + "type": "integer", + "description": "ShardCount is the number of .safetensors shards for a sharded model (1 for a\nsingle-file model)." + }, + "userMetadata": { + "$ref": "#/$defs/KeyValues", + "description": "UserMetadata is the optional \"__metadata__\" map from a .safetensors file header\n(string-to-string key/values set by the producer)." + }, + "metadataHash": { + "type": "string", + "description": "MetadataHash is an xxhash over the on-disk SafeTensors header (sorted tensor\nentries + __metadata__). It is derived ONLY from the safetensors file bytes." + }, + "parts": { + "items": { + "$ref": "#/$defs/SafetensorsModelInfo" + }, + "type": "array", + "description": "Parts contains metadata from additional SafeTensors shards or OCI layers that\nwere merged into this package during post-processing." + } + }, + "type": "object", + "description": "SafeTensorsModelInfo holds the model details extracted from SafeTensors content." + }, + "Schema": { + "properties": { + "version": { + "type": "string", + "description": "Version is the JSON schema version for this document format." + }, + "url": { + "type": "string", + "description": "URL is the URL to the JSON schema definition document." + } + }, + "type": "object", + "required": [ + "version", + "url" + ], + "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format." + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string", + "description": "ID is a unique identifier for the analyzed source artifact." + }, + "name": { + "type": "string", + "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)." + }, + "version": { + "type": "string", + "description": "Version is the version of the analyzed artifact (e.g., image tag)." + }, + "supplier": { + "type": "string", + "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance." + }, + "type": { + "type": "string", + "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")." + }, + "metadata": { + "description": "Metadata contains additional source-specific metadata." + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "VcpkgManifest": { + "properties": { + "description": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)." + }, + "documentation": { + "type": "string", + "description": "Documentation is the URL to the package's documentation." + }, + "full-version": { + "type": "string", + "description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")." + }, + "version": { + "type": "string", + "description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")." + }, + "port-version": { + "type": "integer", + "description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version." + }, + "maintainers": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Maintainers are the people responsible for maintaining the vcpkg port." + }, + "name": { + "type": "string", + "description": "Name is the package name as declared in the manifest." + }, + "supports": { + "type": "string", + "description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")." + }, + "registry": { + "$ref": "#/$defs/VcpkgRegistryEntry", + "description": "Registry indicates where the package definition came from." + }, + "triplet": { + "type": "string", + "description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")." + } + }, + "type": "object", + "required": [ + "full-version", + "version", + "port-version", + "name" + ], + "description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package." + }, + "VcpkgRegistryEntry": { + "properties": { + "baseline": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "packages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "path": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "repository": { + "type": "string" + } + }, + "type": "object", + "required": [ + "kind" + ], + "description": "Matches definition of Vcpkg \"Registry\"." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 75eea2f94..87756c843 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.1.9/document", + "$id": "anchore.io/schema/syft/json/16.1.10/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -2925,6 +2925,9 @@ { "$ref": "#/$defs/RustCargoLockEntry" }, + { + "$ref": "#/$defs/SafetensorsModelInfo" + }, { "$ref": "#/$defs/SnapEntry" }, @@ -4216,6 +4219,55 @@ ], "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." }, + "SafetensorsModelInfo": { + "properties": { + "format": { + "type": "string", + "description": "Format is the source format label (always \"safetensors\" for this metadata type).\nPresent because the Docker AI model config blob carries an explicit format field" + }, + "architecture": { + "type": "string", + "description": "Architecture is the model architecture (e.g., \"LlamaForCausalLM\",\n\"Qwen3MoeForConditionalGeneration\"). It is not present in the SafeTensors\nheader itself; it is enriched from the companion config.json\n\"architectures\" array when one is found alongside the model." + }, + "quantization": { + "type": "string", + "description": "Quantization describes tensor precision (e.g., \"BF16\", \"F16\", \"F32\", \"INT8\")." + }, + "parameters": { + "type": "integer", + "description": "Parameters is the total number of model parameters, computed from the tensor\nshapes in the SafeTensors header(s). For a sharded model it is the sum across\nevery shard." + }, + "tensorCount": { + "type": "integer", + "description": "TensorCount is the number of tensor entries in the file header." + }, + "totalSize": { + "type": "string", + "description": "TotalSize is the total byte size of tensor data across all shards when known\n(from the Docker AI model config \"size\" field)." + }, + "shardCount": { + "type": "integer", + "description": "ShardCount is the number of .safetensors shards for a sharded model (1 for a\nsingle-file model)." + }, + "userMetadata": { + "$ref": "#/$defs/KeyValues", + "description": "UserMetadata is the optional \"__metadata__\" map from a .safetensors file header\n(string-to-string key/values set by the producer)." + }, + "metadataHash": { + "type": "string", + "description": "MetadataHash is an xxhash over the on-disk SafeTensors header (sorted tensor\nentries + __metadata__). It is derived ONLY from the safetensors file bytes." + }, + "parts": { + "items": { + "$ref": "#/$defs/SafetensorsModelInfo" + }, + "type": "array", + "description": "Parts contains metadata from additional SafeTensors shards or OCI layers that\nwere merged into this package during post-processing." + } + }, + "type": "object", + "description": "SafeTensorsModelInfo holds the model details extracted from SafeTensors content." + }, "Schema": { "properties": { "version": { diff --git a/syft/file/mock_resolver.go b/syft/file/mock_resolver.go index b9ed38420..67388796e 100644 --- a/syft/file/mock_resolver.go +++ b/syft/file/mock_resolver.go @@ -25,6 +25,7 @@ type MockResolver struct { mediaTypeIndex map[string][]Location extension map[string][]Location basename map[string][]Location + ociRef string } // NewMockResolverForPaths creates a new MockResolver, where the only resolvable @@ -74,6 +75,16 @@ func NewMockResolverForPathsWithMetadata(metadata map[Coordinates]Metadata) *Moc } } +// NewMockResolverForOCIArtifact creates a MockResolver that can resolve files +// by media type AND surfaces the given OCI ref via the ImageReference method. +// Intended for tests that exercise the catalogers' OCI-artifact-aware naming +// code paths. +func NewMockResolverForOCIArtifact(ref string, mediaTypes map[string][]Location) *MockResolver { + r := NewMockResolverForMediaTypes(mediaTypes) + r.ociRef = ref + return r +} + // NewMockResolverForMediaTypes creates a MockResolver that can resolve files by media type. // The mediaTypes map specifies which locations should be returned for each media type. func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolver { @@ -102,6 +113,11 @@ func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolve } } +// ImageReference returns the image reference associated with this mock, if any. +func (r MockResolver) ImageReference() string { + return r.ociRef +} + // HasPath indicates if the given path exists in the underlying source. func (r MockResolver) HasPath(path string) bool { for _, l := range r.locations { diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index dec9d06f0..cede5bf48 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -58,6 +58,7 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.YarnLockEntry{}, pkg.TerraformLockProviderEntry{}, pkg.GGUFFileHeader{}, + pkg.SafeTensorsModelInfo{}, pkg.DenoLockEntry{}, pkg.DenoRemoteLockEntry{}, pkg.VcpkgManifest{}, diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go index 5682e22da..dfe24324e 100644 --- a/syft/format/internal/spdxutil/helpers/source_info.go +++ b/syft/format/internal/spdxutil/helpers/source_info.go @@ -85,7 +85,7 @@ func SourceInfo(p pkg.Package) string { case pkg.TerraformPkg: answer = "acquired package info from Terraform dependency lock file" case pkg.ModelPkg: - answer = "acquired package info from AI artifact (e.g. GGUF File)" + answer = "acquired package info from AI model artifact" case pkg.VcpkgPkg: answer = "acquired package info from vcpkg manifest file" default: diff --git a/syft/internal/fileresolver/container_image_model.go b/syft/internal/fileresolver/container_image_model.go index 51e6736fd..3cc4a7422 100644 --- a/syft/internal/fileresolver/container_image_model.go +++ b/syft/internal/fileresolver/container_image_model.go @@ -20,16 +20,22 @@ type LayerInfo struct { } // ContainerImageModel is a file.Resolver implementation that provides access to -// GGUF header data fetched from OCI model artifacts via range-GET requests. -// This does not fetch the entire model from the registry, only a sliver of it. +// model header and metadata data (GGUF and SafeTensors headers, the model config +// blob, and companion layers) fetched from OCI model artifacts via range-GET +// requests. This does not fetch the entire model from the registry, only a +// sliver of it. type ContainerImageModel struct { tempDir string // temp directory containing all layer files layerFiles map[string]LayerInfo // digest -> layer info (temp path + media type) locations map[string]file.Location // digest -> location + ref string // image reference the artifact was fetched with } -// NewContainerImageModel creates a new resolver with the given temp directory and layer files. -func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo) *ContainerImageModel { +// NewContainerImageModel creates a new resolver with the given temp directory +// and layer files. The ref is surfaced through the ImageReference method so +// catalogers can derive context-level naming hints from the artifact reference +// when the layer contents don't carry one. +func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo, ref string) *ContainerImageModel { // Create locations for all layer files // Each location has RealPath="/", FileSystemID=digest, AccessPath="/" locations := make(map[string]file.Location, len(layerFiles)) @@ -43,9 +49,15 @@ func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo) *Co tempDir: tempDir, layerFiles: layerFiles, locations: locations, + ref: ref, } } +// ImageReference returns the image reference the artifact was fetched with. +func (r *ContainerImageModel) ImageReference() string { + return r.ref +} + // FilesByMediaType returns locations for layers matching the given media type patterns. // Patterns support glob-style matching (e.g., "application/vnd.docker.ai*"). func (r *ContainerImageModel) FilesByMediaType(types ...string) ([]file.Location, error) { diff --git a/syft/internal/fileresolver/container_image_model_test.go b/syft/internal/fileresolver/container_image_model_test.go index bd8c2efb7..d4a2084b5 100644 --- a/syft/internal/fileresolver/container_image_model_test.go +++ b/syft/internal/fileresolver/container_image_model_test.go @@ -60,7 +60,8 @@ func TestOCIModelResolver_FilesByMediaType(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - resolver := NewContainerImageModel(tempDir, test.layerFiles) + ref := "" + resolver := NewContainerImageModel(tempDir, test.layerFiles, ref) locations, err := resolver.FilesByMediaType(test.patterns...) require.NoError(t, err) @@ -81,7 +82,8 @@ func TestOCIModelResolver_FileContentsByLocation(t *testing.T) { digest: {TempPath: tempFile, MediaType: ggufLayerMediaType}, } - resolver := NewContainerImageModel(tempDir, layerFiles) + ref := "" + resolver := NewContainerImageModel(tempDir, layerFiles, ref) tests := []struct { name string diff --git a/syft/pkg/cataloger/ai/capabilities.yaml b/syft/pkg/cataloger/ai/capabilities.yaml index fff9fa3a7..c7ad601df 100644 --- a/syft/pkg/cataloger/ai/capabilities.yaml +++ b/syft/pkg/cataloger/ai/capabilities.yaml @@ -44,3 +44,46 @@ catalogers: default: true evidence: - GGUFFileHeader.MetadataKeyValuesHash + - ecosystem: ai # MANUAL + name: safetensors-cataloger # AUTO-GENERATED + type: generic # AUTO-GENERATED + source: # AUTO-GENERATED + file: syft/pkg/cataloger/ai/cataloger.go + function: NewSafeTensorsCataloger + selectors: # AUTO-GENERATED + - ai + - directory + - image + - ml + - model + - package + - safetensors + parsers: # AUTO-GENERATED structure + - function: parseSafeTensorsFile + detector: # AUTO-GENERATED + method: glob # AUTO-GENERATED + criteria: # AUTO-GENERATED + - '**/*.safetensors' + metadata_types: # AUTO-GENERATED + - pkg.SafeTensorsModelInfo + package_types: # AUTO-GENERATED + - model + json_schema_types: # AUTO-GENERATED + - SafetensorsModelInfo + capabilities: # MANUAL - preserved across regeneration + - name: license + default: true + - name: dependency.depth + default: [] + - name: dependency.edges + default: "" + - name: dependency.kinds + default: [] + - name: package_manager.files.listing + default: false + - name: package_manager.files.digests + default: false + - name: package_manager.package_integrity_hash + default: true + evidence: + - SafeTensorsModelInfo.MetadataHash diff --git a/syft/pkg/cataloger/ai/cataloger.go b/syft/pkg/cataloger/ai/cataloger.go index d566edba2..5b7a75882 100644 --- a/syft/pkg/cataloger/ai/cataloger.go +++ b/syft/pkg/cataloger/ai/cataloger.go @@ -1,6 +1,6 @@ /* Package ai provides concrete Cataloger implementations for AI artifacts and machine learning models, -including support for GGUF (GPT-Generated Unified Format) model files. +including support for GGUF (GPT-Generated Unified Format) and SafeTensors model files. */ package ai @@ -10,8 +10,9 @@ import ( ) const ( - catalogerName = "gguf-cataloger" - ggufLayerMediaType = "application/vnd.docker.ai*" + catalogerName = "gguf-cataloger" + ggufLayerMediaType = "application/vnd.docker.ai*" + safeTensorsCatalogerName = "safetensors-cataloger" ) // NewGGUFCataloger returns a new cataloger instance for GGUF model files. @@ -23,3 +24,26 @@ func NewGGUFCataloger() pkg.Cataloger { WithParserByMediaType(parseGGUFModel, ggufLayerMediaType). WithProcessors(ggufMergeProcessor) } + +// NewSafeTensorsCataloger returns a cataloger for SafeTensors model files, +// covering three discovery paths: +// - **/*.safetensors files (single-file models and individual shards; +// header-only parse) +// - application/vnd.docker.ai.model.config.v0.1+json / v0.2+json OCI layers +// (Docker Model Runner artifacts whose config advertises format=="safetensors") +// - application/vnd.docker.ai.safetensors OCI layers (per-shard JSON headers, +// fetched as a prefix by the OCI model source; emitted as nameless +// packages and merged into the config-derived package as Parts) +// +// model.safetensors.index.json files are intentionally not parsed today: the +// index describes how tensors map to shards but contributes no metadata the +// cataloger can't derive from the shard headers themselves. If a model is +// distributed as just an index.json with no accompanying shard files, the +// cataloger emits nothing for that directory. +func NewSafeTensorsCataloger() pkg.Cataloger { + return generic.NewCataloger(safeTensorsCatalogerName). + WithParserByGlobs(parseSafeTensorsFile, "**/*.safetensors"). + WithParserByMediaType(parseSafeTensorsOCIConfig, dockerAIModelConfigMediaTypes...). + WithParserByMediaType(parseSafeTensorsOCILayer, dockerAISafeTensorsMediaType). + WithResolvingProcessors(safeTensorsMergeProcessor) +} diff --git a/syft/pkg/cataloger/ai/gguf_processor.go b/syft/pkg/cataloger/ai/gguf_processor.go new file mode 100644 index 000000000..fe31e3f8b --- /dev/null +++ b/syft/pkg/cataloger/ai/gguf_processor.go @@ -0,0 +1,56 @@ +package ai + +import ( + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" +) + +// ggufMergeProcessor consolidates multiple GGUF packages into a single package +// representing the AI model. When scanning OCI images with multiple layers, +// each layer may produce a separate package. This processor finds the package +// with a name and merges metadata from nameless packages into its GGUFFileParts field. +// Only packages with a non-empty name are returned in the final result. +func ggufMergeProcessor(pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { + if err != nil { + return pkgs, rels, err + } + if len(pkgs) == 0 { + return pkgs, rels, err + } + + // Separate packages with names from those without + var namedPkgs []pkg.Package + var namelessHeaders []pkg.GGUFFileHeader + + for _, p := range pkgs { + if p.Name != "" { + namedPkgs = append(namedPkgs, p) + } else { + if header, ok := p.Metadata.(pkg.GGUFFileHeader); ok { + // We do not want a kv hash for nameless headers + header.MetadataKeyValuesHash = "" + namelessHeaders = append(namelessHeaders, header) + } + } + } + + // If there are no named packages, return nothing + if len(namedPkgs) == 0 { + return nil, rels, err + } + + // merge nameless headers into a single named package; + // if there are multiple named packages, return them without trying to merge headers. + // we cannot determine which nameless headers belong to which package + // this is because the order we receive the gguf headers in is not guaranteed + // to match the layer order in the original oci image + if len(namedPkgs) == 1 && len(namelessHeaders) > 0 { + winner := &namedPkgs[0] + if header, ok := winner.Metadata.(pkg.GGUFFileHeader); ok { + header.Parts = namelessHeaders + winner.Metadata = header + } + } + + return namedPkgs, rels, err +} diff --git a/syft/pkg/cataloger/ai/huggingface.go b/syft/pkg/cataloger/ai/huggingface.go new file mode 100644 index 000000000..6c333835b --- /dev/null +++ b/syft/pkg/cataloger/ai/huggingface.go @@ -0,0 +1,93 @@ +package ai + +import ( + "bytes" + + "gopkg.in/yaml.v3" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" +) + +// hfConfig is a minimal projection of Hugging Face config.json fields. +type hfConfig struct { + Architectures []string `json:"architectures"` + NameOrPath string `json:"_name_or_path"` +} + +func applyHFConfig(md *pkg.SafeTensorsModelInfo, cfg *hfConfig) { + if md.Architecture == "" && len(cfg.Architectures) > 0 { + md.Architecture = cfg.Architectures[0] + } +} + +// readmeFrontmatter holds the subset of YAML frontmatter fields we extract. +type readmeFrontmatter struct { + License string `yaml:"license"` + BaseModel []string `yaml:"base_model"` +} + +type licenseFrontmatter struct { + SPDXID string `yaml:"spdx-id"` +} + +// extractFrontmatterBlock returns the YAML bytes between the first and second +// "---" delimiters of a file +func extractFrontmatterBlock(buf []byte) []byte { + trimmed := bytes.TrimLeft(buf, "\xef\xbb\xbf \t\r\n") + if !bytes.HasPrefix(trimmed, []byte("---")) { + return nil + } + rest := trimmed[3:] + if i := bytes.IndexByte(rest, '\n'); i >= 0 { + rest = rest[i+1:] + } + block, _, found := bytes.Cut(rest, []byte("\n---")) + if !found { + return nil + } + return block +} + +// parseFrontmatter decodes a Hugging Face model card YAML frontmatter block +// and returns the license and base_model fields. +func parseFrontmatter(buf []byte) *readmeFrontmatter { + block := extractFrontmatterBlock(buf) + if block == nil { + return nil + } + + var raw struct { + License string `yaml:"license"` + BaseModel yaml.Node `yaml:"base_model"` + } + if err := yaml.Unmarshal(block, &raw); err != nil { + log.Debugf("failed to parse README frontmatter: %v", err) + return nil + } + + fm := readmeFrontmatter{License: raw.License} + switch raw.BaseModel.Kind { + case yaml.ScalarNode: + if raw.BaseModel.Value != "" { + fm.BaseModel = []string{raw.BaseModel.Value} + } + case yaml.SequenceNode: + _ = raw.BaseModel.Decode(&fm.BaseModel) + } + return &fm +} + +// parseLicenseFrontmatter returns the producer-declared SPDX identifier +func parseLicenseFrontmatter(buf []byte) string { + block := extractFrontmatterBlock(buf) + if block == nil { + return "" + } + var fm licenseFrontmatter + if err := yaml.Unmarshal(block, &fm); err != nil { + log.Debugf("failed to parse license frontmatter: %v", err) + return "" + } + return fm.SPDXID +} diff --git a/syft/pkg/cataloger/ai/identity_dir.go b/syft/pkg/cataloger/ai/identity_dir.go new file mode 100644 index 000000000..eaa0d1efa --- /dev/null +++ b/syft/pkg/cataloger/ai/identity_dir.go @@ -0,0 +1,93 @@ +package ai + +import ( + "context" + "encoding/json" + "io" + "path" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +// resolveSafeTensorsDirIdentity handles the directory-scan case for safe tensors +// find config.json beside the model files (walking up parent directories to the +// scanned source root if no sibling exists) and a sibling README.md. It returns +// the group's name candidates, resolved licenses, and supporting evidence. +func resolveSafeTensorsDirIdentity(ctx context.Context, resolver file.Resolver, dir string, md *pkg.SafeTensorsModelInfo) safeTensorsIdentity { + id := safeTensorsIdentity{fallbackName: safeTensorsDirName(dir)} + + if loc, cfg := findDirHFConfig(resolver, dir); cfg != nil { + applyHFConfig(md, cfg) + id.nameOrPath = cfg.NameOrPath + id.supporting = append(id.supporting, *loc) + } + + if loc, fm := readDirReadmeFrontmatter(resolver, path.Join(dir, "README.md")); fm != nil { + if fm.License != "" { + id.licenses = pkg.NewLicensesFromValuesWithContext(ctx, fm.License) + } + if id.nameOrPath == "" && len(fm.BaseModel) > 0 { + id.nameOrPath = fm.BaseModel[0] + } + id.supporting = append(id.supporting, *loc) + } + return id +} + +// findDirHFConfig looks for a config.json beside the model files +func findDirHFConfig(resolver file.Resolver, dir string) (*file.Location, *hfConfig) { + for { + if loc, cfg := readDirHFConfig(resolver, path.Join(dir, "config.json")); cfg != nil { + return loc, cfg + } + parent := path.Dir(dir) + if parent == dir { + return nil, nil // reached the source root + } + dir = parent + } +} + +func readDirHFConfig(resolver file.Resolver, p string) (*file.Location, *hfConfig) { + locations, err := resolver.FilesByPath(p) + if err != nil || len(locations) == 0 { + return nil, nil + } + rc, err := resolver.FileContentsByLocation(locations[0]) + if err != nil { + return nil, nil + } + defer internal.CloseAndLogError(rc, p) + + var cfg hfConfig + if err := json.NewDecoder(rc).Decode(&cfg); err != nil { + log.Debugf("failed to decode %s: %v", p, err) + return nil, nil + } + return &locations[0], &cfg +} + +func readDirReadmeFrontmatter(resolver file.Resolver, p string) (*file.Location, *readmeFrontmatter) { + locations, err := resolver.FilesByPath(p) + if err != nil || len(locations) == 0 { + return nil, nil + } + rc, err := resolver.FileContentsByLocation(locations[0]) + if err != nil { + return nil, nil + } + defer internal.CloseAndLogError(rc, p) + + buf, err := io.ReadAll(io.LimitReader(rc, 1024*1024)) + if err != nil { + return nil, nil + } + fm := parseFrontmatter(buf) + if fm == nil { + return nil, nil + } + return &locations[0], fm +} diff --git a/syft/pkg/cataloger/ai/identity_oci.go b/syft/pkg/cataloger/ai/identity_oci.go new file mode 100644 index 000000000..6209a3f8b --- /dev/null +++ b/syft/pkg/cataloger/ai/identity_oci.go @@ -0,0 +1,173 @@ +package ai + +import ( + "bytes" + "context" + "encoding/json" + "io" + "path" + + gcrname "github.com/google/go-containerregistry/pkg/name" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/licenses" +) + +// resolveSafeTensorsOCIIdentity handles the OCI-artifact case +func resolveSafeTensorsOCIIdentity(ctx context.Context, resolver file.Resolver, md *pkg.SafeTensorsModelInfo) safeTensorsIdentity { + ociResolver, ok := resolver.(file.OCIMediaTypeResolver) + if !ok { + return safeTensorsIdentity{} + } + + modelFileLocs, err := ociResolver.FilesByMediaType(dockerAIModelFileMediaType) + if err != nil { + log.Debugf("failed to list docker AI model-file layers: %v", err) + } + + // Collect config / readme candidates separately so the layer-iteration order + // returned by the resolver doesn't decide the precedence. + var configName, readmeName, readmeLicense string + var supporting []file.Location + for _, loc := range modelFileLocs { + cfg, fm := classifyOCIModelFileLayer(resolver, loc) + switch { + case cfg != nil: + applyHFConfig(md, cfg) + if configName == "" { + configName = cfg.NameOrPath + } + supporting = append(supporting, loc) + case fm != nil: + if readmeLicense == "" { + readmeLicense = fm.License + } + if readmeName == "" && len(fm.BaseModel) > 0 { + readmeName = fm.BaseModel[0] + } + supporting = append(supporting, loc) + } + } + + // Precedence: config.json _name_or_path > README base_model. + nameOrPath := configName + if nameOrPath == "" { + nameOrPath = readmeName + } + + id := safeTensorsIdentity{ + nameOrPath: nameOrPath, + fallbackName: ociImageRefBasename(resolver), + supporting: supporting, + } + + // License precedence: a dedicated vnd.docker.ai.license layer is a + // outranks the free-text license field in a model card's README frontmatter. + licLocs, err := ociResolver.FilesByMediaType(dockerAILicenseMediaType) + if err != nil { + log.Debugf("failed to list docker AI license layers: %v", err) + } + switch { + case len(licLocs) > 0: + id.licenses = identifyLicenseLayers(ctx, resolver, licLocs) + id.supporting = append(id.supporting, licLocs...) + case readmeLicense != "": + id.licenses = pkg.NewLicensesFromValuesWithContext(ctx, readmeLicense) + } + + return id +} + +// ociImageReferencer is the minimal capability ociImageRefBasename needs: a +// resolver that can surface the OCI image reference it was built from. It is +// kept local to this package (rather than exported from the file package) so the +// assertion stays with its only consumer. +type ociImageReferencer interface { + ImageReference() string +} + +func ociImageRefBasename(resolver file.Resolver) string { + // TODO: we don't think this approach is generalizable quite yet, but we really do need this information. + // (Ideally we should be NOT be type asserting on the file resolver directly). + info, ok := resolver.(ociImageReferencer) + if !ok { + return "" + } + ref := info.ImageReference() + if ref == "" { + return "" + } + parsed, err := gcrname.ParseReference(ref) + if err != nil { + log.Debugf("failed to parse OCI ref %q: %v", ref, err) + return "" + } + return path.Base(parsed.Context().RepositoryStr()) +} + +// classifyOCIModelFileLayer reads up to 4 MiB of a model.file layer and decodes +// it as either an HF config.json or a README model card's YAML frontmatter, +// based on its leading bytes. It returns whichever it recognized; both are nil +// when the layer is neither (or fails to decode). The caller owns precedence and +// metadata enrichment. +func classifyOCIModelFileLayer(resolver file.Resolver, loc file.Location) (*hfConfig, *readmeFrontmatter) { + rc, err := resolver.FileContentsByLocation(loc) + if err != nil { + return nil, nil + } + defer internal.CloseAndLogError(rc, loc.RealPath) + + buf, err := io.ReadAll(io.LimitReader(rc, 4*1024*1024)) + if err != nil { + return nil, nil + } + trimmed := bytes.TrimLeft(buf, "\xef\xbb\xbf \t\r\n") + switch { + case bytes.HasPrefix(trimmed, []byte("---")): + return nil, parseFrontmatter(buf) + case bytes.HasPrefix(trimmed, []byte("{")): + var cfg hfConfig + if err := json.Unmarshal(buf, &cfg); err != nil { + return nil, nil + } + return &cfg, nil + } + return nil, nil +} + +// identifyLicenseLayers turns Docker AI license-layer locations into pkg.License values. +func identifyLicenseLayers(ctx context.Context, resolver file.Resolver, locs []file.Location) []pkg.License { + var out []pkg.License + var scanFallback []file.Location + for i := range locs { + loc := locs[i] + if spdx := readLicenseSPDXIDFromFrontmatter(resolver, loc); spdx != "" { + out = append(out, pkg.NewLicenseFromFieldsWithContext(ctx, spdx, "", &loc)) + continue + } + scanFallback = append(scanFallback, loc) + } + if len(scanFallback) > 0 { + out = append(out, licenses.FindAtLocations(ctx, resolver, scanFallback...)...) + } + return out +} + +// readLicenseSPDXIDFromFrontmatter reads a bounded prefix of a license-layer +// blob and returns the spdx-id declared in its YAML frontmatter +func readLicenseSPDXIDFromFrontmatter(resolver file.Resolver, loc file.Location) string { + rc, err := resolver.FileContentsByLocation(loc) + if err != nil { + return "" + } + defer internal.CloseAndLogError(rc, loc.RealPath) + + buf, err := io.ReadAll(io.LimitReader(rc, 64*1024)) + if err != nil { + return "" + } + return parseLicenseFrontmatter(buf) +} diff --git a/syft/pkg/cataloger/ai/merge.go b/syft/pkg/cataloger/ai/merge.go new file mode 100644 index 000000000..a8d2b4e74 --- /dev/null +++ b/syft/pkg/cataloger/ai/merge.go @@ -0,0 +1,139 @@ +package ai + +import ( + "fmt" + "sort" + "strings" + + "github.com/cespare/xxhash/v2" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +// mergeSafeTensorsGroup folds a group's per-member metadata into a single package. +func mergeSafeTensorsGroup(members []pkg.Package) pkg.Package { + locSet := unionLocations(members) + aggregates, shards := bucketSafeTensorsMembers(members) + + merged := pkg.SafeTensorsModelInfo{Format: "safetensors"} + mergeAggregatesInto(&merged, aggregates) + shardTensorTotal, hashes := mergeShardsInto(&merged, shards) + + // Keep merged UserMetadata globally key-sorted so the SBOM is stable + sort.Slice(merged.UserMetadata, func(i, j int) bool { + return merged.UserMetadata[i].Key < merged.UserMetadata[j].Key + }) + + if merged.TensorCount == 0 { + merged.TensorCount = shardTensorTotal + } + if merged.ShardCount == 0 { + if len(shards) > 0 { + merged.ShardCount = len(shards) + } else { + merged.ShardCount = 1 + } + } + merged.MetadataHash = rollupHash(hashes) + + // Parts only carry value for multi-shard models; for a single shard the + // outer view already exposes every per-shard field. + if len(shards) > 1 { + parts := append([]pkg.SafeTensorsModelInfo(nil), shards...) + sort.Slice(parts, func(i, j int) bool { + return parts[i].MetadataHash < parts[j].MetadataHash + }) + merged.Parts = parts + } + + return pkg.Package{ + Locations: locSet, + Type: pkg.ModelPkg, + Metadata: merged, + } +} + +func mergeAggregatesInto(merged *pkg.SafeTensorsModelInfo, aggregates []pkg.SafeTensorsModelInfo) { + for _, a := range aggregates { + if merged.TensorCount == 0 { + merged.TensorCount = a.TensorCount + } + firstNonEmpty(&merged.TotalSize, a.TotalSize) + firstNonEmpty(&merged.Quantization, a.Quantization) + } +} + +// mergeShardsInto folds the per-shard header metadata into merged, returning +// the summed shard TensorCount and the list of non-empty per-shard hashes for +// the rollup. Shards carry only the content-derived fields (Quantization, +// Parameters, UserMetadata), so those are the only fields folded in here. +// TensorCount and Parameters are summed because each shard holds a distinct +// slice of the model; Quantization takes the first value since all shards share +// one precision. +func mergeShardsInto(merged *pkg.SafeTensorsModelInfo, shards []pkg.SafeTensorsModelInfo) (shardTensorTotal uint64, hashes []string) { + seenKV := map[string]bool{} + for _, s := range shards { + shardTensorTotal += s.TensorCount + merged.Parameters += s.Parameters + firstNonEmpty(&merged.Quantization, s.Quantization) + for _, kv := range s.UserMetadata { + if seenKV[kv.Key] { + continue + } + seenKV[kv.Key] = true + merged.UserMetadata = append(merged.UserMetadata, kv) + } + if s.MetadataHash != "" { + hashes = append(hashes, s.MetadataHash) + } + } + return shardTensorTotal, hashes +} + +func firstNonEmpty(dst *string, v string) { + if *dst == "" { + *dst = v + } +} + +// unionLocations gathers every location from every member into a single set. +func unionLocations(members []pkg.Package) file.LocationSet { + out := file.NewLocationSet() + for _, m := range members { + for _, l := range m.Locations.ToSlice() { + out.Add(l) + } + } + return out +} + +// bucketSafeTensorsMembers splits group members into aggregate-flavored entries +// (no MetadataHash — Docker AI config blob or sharded index) and shard-flavored +// entries (carry a content-derived MetadataHash from a header parser). +func bucketSafeTensorsMembers(members []pkg.Package) (aggregates, shards []pkg.SafeTensorsModelInfo) { + for _, m := range members { + md, ok := m.Metadata.(pkg.SafeTensorsModelInfo) + if !ok { + continue + } + if md.MetadataHash != "" { + shards = append(shards, md) + continue + } + aggregates = append(aggregates, md) + } + return aggregates, shards +} + +func rollupHash(hashes []string) string { + if len(hashes) == 0 { + return "" + } + if len(hashes) == 1 { + return hashes[0] + } + sorted := append([]string(nil), hashes...) + sort.Strings(sorted) + return fmt.Sprintf("%016x", xxhash.Sum64String(strings.Join(sorted, "|"))) +} diff --git a/syft/pkg/cataloger/ai/merge_test.go b/syft/pkg/cataloger/ai/merge_test.go new file mode 100644 index 000000000..7276d8896 --- /dev/null +++ b/syft/pkg/cataloger/ai/merge_test.go @@ -0,0 +1,166 @@ +package ai + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +// stPkg builds a model package carrying the given metadata, with each path +// recorded as a primary-evidence location. +func stPkg(md pkg.SafeTensorsModelInfo, paths ...string) pkg.Package { + locs := make([]file.Location, 0, len(paths)) + for _, p := range paths { + locs = append(locs, file.NewLocation(p).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + } + return pkg.Package{Type: pkg.ModelPkg, Metadata: md, Locations: file.NewLocationSet(locs...)} +} + +// shardMeta is a content-derived shard entry: it carries a MetadataHash, which is +// what marks a group member as a shard (vs. a hash-less aggregate config blob). +func shardMeta(hash string, tensorCount uint64) pkg.SafeTensorsModelInfo { + return pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: tensorCount, + Quantization: "BF16", + Parameters: 1000, + MetadataHash: hash, + } +} + +// TestMergeSafeTensorsGroup exercises the rollup contract directly (the cataloger +// tests cover it only as a side effect of the merge processor). It locks how a +// group's per-member metadata folds into one package: tensor-count summing, +// aggregate-over-shard field precedence, UserMetadata dedup + sorting, Parts +// rollup, ShardCount derivation, and the content MetadataHash rollup. +func TestMergeSafeTensorsGroup(t *testing.T) { + t.Run("single shard: hash passes through, ShardCount 1, no Parts", func(t *testing.T) { + out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 5), "/m/a.safetensors")}) + + md := out.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, pkg.ModelPkg, out.Type) + assert.Equal(t, 1, md.ShardCount) + assert.Equal(t, uint64(5), md.TensorCount) + assert.Equal(t, "aaaa", md.MetadataHash, "a single shard's hash passes through unchanged") + assert.Nil(t, md.Parts, "single-shard models do not populate Parts") + }) + + t.Run("multi-shard: tensors summed, Parts sorted by hash, rollup is order-independent", func(t *testing.T) { + in := []pkg.Package{ + stPkg(shardMeta("cccc", 3), "/m/c.safetensors"), + stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"), + stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"), + } + out := mergeSafeTensorsGroup(in) + + md := out.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, 3, md.ShardCount) + assert.Equal(t, uint64(9), md.TensorCount, "tensor counts are summed across shards") + assert.Equal(t, uint64(3000), md.Parameters, "parameter counts are summed across shards") + require.Len(t, md.Parts, 3) + assert.Equal(t, + []string{"aaaa", "bbbb", "cccc"}, + []string{md.Parts[0].MetadataHash, md.Parts[1].MetadataHash, md.Parts[2].MetadataHash}, + "Parts are sorted by metadata hash", + ) + assert.Equal(t, rollupHash([]string{"aaaa", "bbbb", "cccc"}), md.MetadataHash) + + // the rollup hash must not depend on the order members arrive in + shuffled := []pkg.Package{ + stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"), + stPkg(shardMeta("cccc", 3), "/m/c.safetensors"), + stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"), + } + out2 := mergeSafeTensorsGroup(shuffled) + assert.Equal(t, md.MetadataHash, out2.Metadata.(pkg.SafeTensorsModelInfo).MetadataHash) + }) + + t.Run("aggregate fields win over shard-derived fields", func(t *testing.T) { + // an aggregate (OCI config blob) carries no MetadataHash but declares the + // authoritative totals. + aggregate := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: 999, + TotalSize: "5.00GB", + Quantization: "Q4_K_M", + } + in := []pkg.Package{ + stPkg(aggregate, "/"), + stPkg(shardMeta("aaaa", 3), "/"), + stPkg(shardMeta("bbbb", 3), "/"), + } + out := mergeSafeTensorsGroup(in) + + md := out.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, uint64(999), md.TensorCount, "aggregate TensorCount is authoritative; shard counts are not summed in") + assert.Equal(t, "5.00GB", md.TotalSize) + assert.Equal(t, uint64(2000), md.Parameters, "parameters are always measured from the shards (summed), not taken from the aggregate") + assert.Equal(t, "Q4_K_M", md.Quantization, "aggregate quantization wins over the shard dtype") + assert.Equal(t, 2, md.ShardCount, "ShardCount comes from the number of shards, not the aggregate") + assert.Equal(t, rollupHash([]string{"aaaa", "bbbb"}), md.MetadataHash, "the content hash still rolls up the shard hashes") + }) + + t.Run("aggregate-only group: ShardCount 1, empty hash, no Parts", func(t *testing.T) { + aggregate := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 42, TotalSize: "1.00GB"} + out := mergeSafeTensorsGroup([]pkg.Package{stPkg(aggregate, "/")}) + + md := out.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, uint64(42), md.TensorCount) + assert.Equal(t, 1, md.ShardCount, "a group with no shards still reports a single shard") + assert.Equal(t, "", md.MetadataHash, "there are no shard hashes to roll up") + assert.Nil(t, md.Parts) + }) + + t.Run("UserMetadata: keys merged and sorted, first value wins on conflict", func(t *testing.T) { + // keys are intentionally unsorted within each shard so the assertion proves + // the merge re-sorts globally; "format" appears in both shards so dedup + // precedence (first wins) is exercised too. + s1 := shardMeta("aaaa", 1) + s1.UserMetadata = pkg.KeyValues{{Key: "format", Value: "pt"}, {Key: "author", Value: "alice"}} + s2 := shardMeta("bbbb", 1) + s2.UserMetadata = pkg.KeyValues{{Key: "format", Value: "gguf"}, {Key: "license", Value: "mit"}} + + out := mergeSafeTensorsGroup([]pkg.Package{stPkg(s1, "/m/a.safetensors"), stPkg(s2, "/m/b.safetensors")}) + + md := out.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, pkg.KeyValues{ + {Key: "author", Value: "alice"}, + {Key: "format", Value: "pt"}, // first shard's value wins over s2's "gguf" + {Key: "license", Value: "mit"}, + }, md.UserMetadata) + }) + + t.Run("members without safetensors metadata are ignored in the rollup", func(t *testing.T) { + notST := pkg.Package{ + Type: pkg.ModelPkg, + Metadata: pkg.GGUFFileHeader{}, + Locations: file.NewLocationSet(file.NewLocation("/m/x.gguf")), + } + out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 2), "/m/a.safetensors"), notST}) + + md := out.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, uint64(2), md.TensorCount, "only the safetensors shard contributes") + assert.Equal(t, 1, md.ShardCount) + assert.Equal(t, "aaaa", md.MetadataHash) + }) +} + +// TestRollupHash locks the cross-source content-fingerprint rollup: empty input +// yields no hash, a lone shard's hash passes through unchanged (so a single-shard +// model fingerprints identically across directory and OCI sources), and multiple +// shards fold into one order-independent digest. +func TestRollupHash(t *testing.T) { + assert.Equal(t, "", rollupHash(nil), "no hashes → empty") + assert.Equal(t, "solo", rollupHash([]string{"solo"}), "a single hash passes through unchanged") + + ab := rollupHash([]string{"a", "b"}) + ba := rollupHash([]string{"b", "a"}) + assert.Equal(t, ab, ba, "the rollup is independent of input order") + assert.Len(t, ab, 16, "a multi-hash rollup is a 16-char xxhash") + assert.NotEqual(t, "a", ab) + assert.NotEqual(t, "b", ab) +} diff --git a/syft/pkg/cataloger/ai/naming.go b/syft/pkg/cataloger/ai/naming.go new file mode 100644 index 000000000..ed2571dcb --- /dev/null +++ b/syft/pkg/cataloger/ai/naming.go @@ -0,0 +1,22 @@ +package ai + +import "path" + +// pickSafeTensorsName implements the documented naming precedence chain: +func pickSafeTensorsName(nameOrPath, fallbackName string) string { + if nameOrPath != "" { + return path.Base(nameOrPath) + } + return fallbackName +} + +// safeTensorsDirName returns the directory-scan naming fallback: the base name +// of the group's parent directory (the group key is already that directory). +func safeTensorsDirName(directory string) string { + base := path.Base(directory) + switch base { + case "/", ".", "": + return "" + } + return base +} diff --git a/syft/pkg/cataloger/ai/package.go b/syft/pkg/cataloger/ai/package.go index 67c6570ae..3abb42343 100644 --- a/syft/pkg/cataloger/ai/package.go +++ b/syft/pkg/cataloger/ai/package.go @@ -20,3 +20,17 @@ func newGGUFPackage(metadata *pkg.GGUFFileHeader, modelName, version, license st return p } + +// newSafeTensorsPackage creates a SafeTensors package with the given metadata +// and locations. Name and Licenses are intentionally not set here and done at the processor level +func newSafeTensorsPackage(metadata *pkg.SafeTensorsModelInfo, locations ...file.Location) pkg.Package { + p := pkg.Package{ + Locations: file.NewLocationSet(locations...), + Type: pkg.ModelPkg, + Metadata: *metadata, + // PURL is intentionally not set: package-url has not yet finalized ML model support. + } + p.SetID() + + return p +} diff --git a/syft/pkg/cataloger/ai/parse_safetensors.go b/syft/pkg/cataloger/ai/parse_safetensors.go new file mode 100644 index 000000000..bc1ff1402 --- /dev/null +++ b/syft/pkg/cataloger/ai/parse_safetensors.go @@ -0,0 +1,202 @@ +package ai + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "sort" + "strings" + + "github.com/cespare/xxhash/v2" + + "github.com/anchore/syft/syft/pkg" +) + +// SafeTensors file format: [8 bytes u64 LE header size] [N bytes JSON header] [tensor data]. +// Reference: https://github.com/huggingface/safetensors#format +const ( + maxSafeTensorsHeaderSize = 100 * 1024 * 1024 // 100MB ceiling on header JSON to prevent OOM +) + +// safeTensorsHeader is the decoded JSON header. Tensor entries live alongside a +// reserved "__metadata__" key holding a string-to-string producer map. We decode +// tensor entries into a generic map so we can iterate and count without a fixed +// schema for every field. +type safeTensorsHeader struct { + metadata map[string]string + tensors map[string]safeTensorsEntry +} + +// safeTensorsEntry describes a single tensor within the header JSON. +type safeTensorsEntry struct { + DType string `json:"dtype"` + Shape []int64 `json:"shape"` + DataOffsets []int64 `json:"data_offsets"` +} + +// readSafeTensorsHeader reads and parses the JSON header from a .safetensors +// file (the leading `[8-byte LE length] [length bytes of JSON]` block) and +// returns the decoded header. +func readSafeTensorsHeader(r io.Reader) (*safeTensorsHeader, error) { + var lenBuf [8]byte + if _, err := io.ReadFull(r, lenBuf[:]); err != nil { + return nil, fmt.Errorf("failed to read header length: %w", err) + } + headerLen := binary.LittleEndian.Uint64(lenBuf[:]) + if headerLen == 0 { + return nil, fmt.Errorf("safetensors header length is zero") + } + if headerLen > maxSafeTensorsHeaderSize { + return nil, fmt.Errorf("safetensors header size %d exceeds maximum %d", headerLen, maxSafeTensorsHeaderSize) + } + + // Read incrementally rather than pre-allocating headerLen up front + body, err := io.ReadAll(io.LimitReader(r, int64(headerLen))) + if err != nil { + return nil, fmt.Errorf("failed to read header body: %w", err) + } + if uint64(len(body)) != headerLen { + return nil, fmt.Errorf("safetensors header truncated: read %d of %d bytes", len(body), headerLen) + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(body, &raw); err != nil { + return nil, fmt.Errorf("failed to decode safetensors header JSON: %w", err) + } + + h := &safeTensorsHeader{tensors: make(map[string]safeTensorsEntry, len(raw))} + for key, val := range raw { + if key == "__metadata__" { + if err := json.Unmarshal(val, &h.metadata); err != nil { + return nil, fmt.Errorf("failed to decode __metadata__: %w", err) + } + continue + } + var entry safeTensorsEntry + if err := json.Unmarshal(val, &entry); err != nil { + // Not all entries must conform; skip anything we cannot decode rather than fail. + continue + } + h.tensors[key] = entry + } + + return h, nil +} + +// parameterCount sums the element counts across all tensors in the header. +func (h *safeTensorsHeader) parameterCount() uint64 { + var total uint64 + for _, t := range h.tensors { + count := uint64(1) + for _, dim := range t.Shape { + if dim <= 0 { + count = 0 + break + } + count *= uint64(dim) + } + total += count + } + return total +} + +// dominantDType returns the dtype that accounts for the largest fraction of parameters. +// For mixed-precision models the "dominant" dtype is still a useful summary. +func (h *safeTensorsHeader) dominantDType() string { + sizeByDType := make(map[string]uint64) + for _, t := range h.tensors { + count := uint64(1) + for _, dim := range t.Shape { + if dim <= 0 { + count = 0 + break + } + count *= uint64(dim) + } + sizeByDType[t.DType] += count + } + var best string + var bestSize uint64 + for dtype, size := range sizeByDType { + if size > bestSize || (size == bestSize && dtype < best) { + best = dtype + bestSize = size + } + } + return best +} + +// metadataHash returns a stable xxhash64 over the logical tensor content +// (name + dtype + shape) plus the __metadata__ map. Tensor keys are sorted to +// keep the hash deterministic across producers. +func (h *safeTensorsHeader) metadataHash() string { + type logicalEntry struct { + Name string `json:"name"` + DType string `json:"dtype"` + Shape []int64 `json:"shape"` + } + entries := make([]logicalEntry, 0, len(h.tensors)) + for name, t := range h.tensors { + entries = append(entries, logicalEntry{Name: name, DType: t.DType, Shape: t.Shape}) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) + + type hashInput struct { + Tensors []logicalEntry `json:"tensors"` + Metadata map[string]string `json:"metadata,omitempty"` + } + b, err := json.Marshal(hashInput{Tensors: entries, Metadata: h.metadata}) + if err != nil { + return "" + } + return fmt.Sprintf("%016x", xxhash.Sum64(b)) +} + +// userMetadataKeyValues converts the safetensors __metadata__ map into a +// KeyValues slice sorted by key, so SBOM output is stable across runs. Returns +// nil for empty input (omitempty then drops the field). +func userMetadataKeyValues(m map[string]string) pkg.KeyValues { + if len(m) == 0 { + return nil + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + out := make(pkg.KeyValues, 0, len(keys)) + for _, k := range keys { + out = append(out, pkg.KeyValue{Key: k, Value: m[k]}) + } + return out +} + +// normalizeDType maps a safetensors/torch dtype label to an uppercase quantization +// shorthand matching conventions used elsewhere in syft (e.g., BF16, F16, I8). +func normalizeDType(dtype string) string { + switch strings.ToUpper(dtype) { + case "BF16": + return "BF16" + case "F16", "FP16", "FLOAT16", "HALF": + return "F16" + case "F32", "FP32", "FLOAT32", "FLOAT": + return "F32" + case "F64", "FP64", "FLOAT64", "DOUBLE": + return "F64" + case "I8", "INT8": + return "I8" + case "U8", "UINT8": + return "U8" + case "I16", "INT16": + return "I16" + case "I32", "INT32": + return "I32" + case "I64", "INT64": + return "I64" + case "BOOL": + return "BOOL" + default: + return strings.ToUpper(dtype) + } +} diff --git a/syft/pkg/cataloger/ai/parse_safetensors_model.go b/syft/pkg/cataloger/ai/parse_safetensors_model.go new file mode 100644 index 000000000..a9a8f3c66 --- /dev/null +++ b/syft/pkg/cataloger/ai/parse_safetensors_model.go @@ -0,0 +1,48 @@ +package ai + +import ( + "context" + "fmt" + "io" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/unknown" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +// parseSafeTensorsFile decodes the JSON header of a single .safetensors file +// (also called once per shard for sharded models) and emits a nameless package +// whose metadata is derived purely from the header bytes. Naming, license +// resolution, sibling enrichment, and cross-shard rollup are all handled by +// safeTensorsMergeProcessor. +func parseSafeTensorsFile(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + defer internal.CloseAndLogError(reader, reader.Path()) + + header, err := readSafeTensorsHeader(&io.LimitedReader{R: reader, N: maxSafeTensorsHeaderSize + 8}) + if err != nil { + return nil, nil, fmt.Errorf("failed to read safetensors header: %w", err) + } + + // ShardCount is intentionally not set here: the merge processor is the single + // owner of ShardCount and derives it from the number of shards in the group. + md := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: uint64(len(header.tensors)), + Parameters: header.parameterCount(), + Quantization: normalizeDType(header.dominantDType()), + UserMetadata: userMetadataKeyValues(header.metadata), + MetadataHash: header.metadataHash(), + } + + p := newSafeTensorsPackage( + &md, + reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + return []pkg.Package{p}, nil, unknown.IfEmptyf([]pkg.Package{p}, "unable to parse safetensors file") +} + +// integrity check +var _ generic.Parser = parseSafeTensorsFile diff --git a/syft/pkg/cataloger/ai/parse_safetensors_oci.go b/syft/pkg/cataloger/ai/parse_safetensors_oci.go new file mode 100644 index 000000000..7fd9f3b3e --- /dev/null +++ b/syft/pkg/cataloger/ai/parse_safetensors_oci.go @@ -0,0 +1,117 @@ +package ai + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/unknown" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +// Docker AI OCI media types used by Docker Model Runner artifacts. +const ( + dockerAIModelFileMediaType = "application/vnd.docker.ai.model.file" + dockerAILicenseMediaType = "application/vnd.docker.ai.license" + dockerAISafeTensorsMediaType = "application/vnd.docker.ai.safetensors" +) + +// dockerAIModelConfigMediaTypes are the model-config schema versions this +// cataloger understands. Versions are enumerated explicitly rather than matched +// with a wildcard so that a future, potentially breaking, config schema is not +// silently consumed; add a new version here only after confirming the fields we +// parse still apply. +var dockerAIModelConfigMediaTypes = []string{ + "application/vnd.docker.ai.model.config.v0.1+json", + "application/vnd.docker.ai.model.config.v0.2+json", +} + +// dockerAIModelConfig mirrors the JSON shape of the vnd.docker.ai.model.config +// blob written by Docker Model Runner for AI artifacts. Only fields we use are +// declared; unknown fields are ignored. +type dockerAIModelConfig struct { + Config struct { + Format string `json:"format"` + Quantization string `json:"quantization"` + Size string `json:"size"` + SafeTensors struct { + TensorCount json.Number `json:"tensor_count"` + } `json:"safetensors"` + } `json:"config"` +} + +// parseSafeTensorsOCIConfig decodes the Docker AI model-config blob +func parseSafeTensorsOCIConfig(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + defer internal.CloseAndLogError(reader, reader.Path()) + + body, err := io.ReadAll(io.LimitReader(reader, 1024*1024)) + if err != nil { + return nil, nil, fmt.Errorf("failed to read docker AI model config: %w", err) + } + + var cfg dockerAIModelConfig + if err := json.Unmarshal(body, &cfg); err != nil { + return nil, nil, fmt.Errorf("failed to decode docker AI model config: %w", err) + } + + if !strings.EqualFold(cfg.Config.Format, "safetensors") { + return nil, nil, nil + } + + // Parameters is intentionally not read from the config blob: we measure the + // true parameter count from the SafeTensors layer headers (parseSafeTensorsOCILayer) + // so OCI and directory scans of the same model agree, rather than trusting the + // producer-supplied label here. + md := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + Quantization: cfg.Config.Quantization, + TotalSize: cfg.Config.Size, + } + if n, err := cfg.Config.SafeTensors.TensorCount.Int64(); err == nil && n > 0 { + md.TensorCount = uint64(n) + } + + p := newSafeTensorsPackage( + &md, + reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + return []pkg.Package{p}, nil, unknown.IfEmptyf([]pkg.Package{p}, "unable to parse docker AI safetensors config") +} + +// parseSafeTensorsOCILayer decodes the JSON header of a SafeTensors weight +// layer fetched from an OCI model artifact +func parseSafeTensorsOCILayer(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + defer internal.CloseAndLogError(reader, reader.Path()) + + header, err := readSafeTensorsHeader(&io.LimitedReader{R: reader, N: maxSafeTensorsHeaderSize + 8}) + if err != nil { + return nil, nil, fmt.Errorf("failed to read safetensors layer header: %w", err) + } + + md := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: uint64(len(header.tensors)), + Parameters: header.parameterCount(), + Quantization: normalizeDType(header.dominantDType()), + UserMetadata: userMetadataKeyValues(header.metadata), + MetadataHash: header.metadataHash(), + } + + p := newSafeTensorsPackage( + &md, + reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + return []pkg.Package{p}, nil, nil +} + +// integrity checks +var ( + _ generic.Parser = parseSafeTensorsOCIConfig + _ generic.Parser = parseSafeTensorsOCILayer +) diff --git a/syft/pkg/cataloger/ai/parse_safetensors_test.go b/syft/pkg/cataloger/ai/parse_safetensors_test.go new file mode 100644 index 000000000..56d50151a --- /dev/null +++ b/syft/pkg/cataloger/ai/parse_safetensors_test.go @@ -0,0 +1,1202 @@ +package ai + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +// buildSafeTensorsFile builds the on-disk bytes of a .safetensors file: an +// 8-byte little-endian header length followed by the JSON header. Tensor data +// is omitted because the parser only reads the header. +func buildSafeTensorsFile(t *testing.T, metadata map[string]string, tensors map[string]safeTensorsEntry) []byte { + t.Helper() + raw := map[string]any{} + if metadata != nil { + raw["__metadata__"] = metadata + } + for name, entry := range tensors { + raw[name] = entry + } + body, err := json.Marshal(raw) + require.NoError(t, err) + + out := make([]byte, 8+len(body)) + binary.LittleEndian.PutUint64(out[:8], uint64(len(body))) + copy(out[8:], body) + return out +} + +// TestSafeTensorsCataloger is the end-to-end `dir:` scan naming matrix: it walks +// real on-disk directory trees and locks how a model package gets its name at +// every depth a .safetensors file can appear (the scan root `./`, an immediate +// child `./sometensor/`, and a grandchild `./dir/someothertensor/`). +// +// The naming precedence (owned by the merge processor's pickSafeTensorsName) is: +// 1. config.json _name_or_path (path.Base applied), found beside the model or +// by walking up parent directories to the scan root +// 2. otherwise the model's immediate parent directory base name +// → drop (no package) when neither yields a usable name +// +// Every model below is built from the same header bytes, so the header-derived +// metadata (Quantization/Parameters/TensorCount/UserMetadata/MetadataHash) is +// identical across rows and each row stays focused on naming. +func TestSafeTensorsCataloger(t *testing.T) { + userMeta := map[string]string{"format": "pt"} + tensors := map[string]safeTensorsEntry{ + "model.embed.weight": {DType: "BF16", Shape: []int64{1000, 16}, DataOffsets: []int64{0, 32000}}, + "model.layer.weight": {DType: "BF16", Shape: []int64{16, 16}, DataOffsets: []int64{32000, 32512}}, + } + // the dedicated hash test below locks the algorithm; here we only assert the + // cataloger wires the header hash through to the package metadata. + wantHash := (&safeTensorsHeader{metadata: userMeta, tensors: tensors}).metadataHash() + + // model writes the shared .safetensors header into dir/.safetensors, + // creating dir if needed. + model := func(t *testing.T, dir string) { + t.Helper() + require.NoError(t, os.MkdirAll(dir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "model.safetensors"), buildSafeTensorsFile(t, userMeta, tensors), 0o644)) + } + writeFile := func(t *testing.T, path, contents string) { + t.Helper() + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(contents), 0o644)) + } + // wantMetadata is the constant header-derived metadata; architecture is the + // only field that varies (it's enriched from config.json when present). + wantMetadata := func(architecture string) pkg.SafeTensorsModelInfo { + return pkg.SafeTensorsModelInfo{ + Format: "safetensors", + Architecture: architecture, + Quantization: "BF16", + Parameters: 16256, + TensorCount: 2, + ShardCount: 1, + UserMetadata: pkg.KeyValues{{Key: "format", Value: "pt"}}, + MetadataHash: wantHash, + } + } + + tests := []struct { + name string + setup func(t *testing.T) string + expectedPackages []pkg.Package + expectedRelationships []artifact.Relationship + }{ + { + // rung 1: config.json _name_or_path (path.Base of "org/Llama-3-8B") + // wins over the "sometensor" directory fallback; license from README. + name: "config.json _name_or_path names the model and wins over the directory", + setup: func(t *testing.T) string { + dir := t.TempDir() + modelDir := filepath.Join(dir, "sometensor") + model(t, modelDir) + writeFile(t, filepath.Join(modelDir, "config.json"), + `{"architectures":["LlamaForCausalLM"],"torch_dtype":"bfloat16","transformers_version":"4.40.0","_name_or_path":"meta-llama/Llama-3-8B"}`) + writeFile(t, filepath.Join(modelDir, "README.md"), + "---\nlicense: Apache-2.0\nbase_model:\n - meta-llama/Llama-3\n---\n# Llama 3\n") + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "Llama-3-8B", + Type: pkg.ModelPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromFields("Apache-2.0", "", nil), + ), + Metadata: wantMetadata("LlamaForCausalLM"), + }, + }, + }, + { + // rung 1 via README: with no config.json, the README model card's + // base_model names the model (path.Base applied), still beating the + // directory fallback ("readme-named"). + name: "README base_model names the model when there is no config.json", + setup: func(t *testing.T) string { + dir := t.TempDir() + modelDir := filepath.Join(dir, "readme-named") + model(t, modelDir) + writeFile(t, filepath.Join(modelDir, "README.md"), + "---\nbase_model:\n - org/base-model-name\n---\n# Card\n") + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "base-model-name", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + }, + }, + { + // rung 2: no config.json at all, so the model is named after its + // immediate parent directory. + name: "no config.json falls back to the parent directory name", + setup: func(t *testing.T) string { + dir := t.TempDir() + model(t, filepath.Join(dir, "sometensor")) + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "sometensor", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + }, + }, + { + // rung 2: config.json exists but carries no _name_or_path (only + // non-identifying info), so we still fall back to the directory name + // while enriching architecture from the config. + name: "config.json without _name_or_path falls back to the parent directory name", + setup: func(t *testing.T) string { + dir := t.TempDir() + modelDir := filepath.Join(dir, "sometensor") + model(t, modelDir) + writeFile(t, filepath.Join(modelDir, "config.json"), + `{"architectures":["LlamaForCausalLM"],"torch_dtype":"bfloat16"}`) + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "sometensor", + Type: pkg.ModelPkg, + Metadata: wantMetadata("LlamaForCausalLM"), + }, + }, + }, + { + // rung 2: the fallback is the IMMEDIATE parent ("someothertensor"), + // not an ancestor ("dir"). + name: "nested model with no config.json is named by its immediate parent directory", + setup: func(t *testing.T) string { + dir := t.TempDir() + model(t, filepath.Join(dir, "dir", "someothertensor")) + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "someothertensor", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + }, + }, + { + // rung 1 at the scan root: a model directly at `./` is unnameable by + // the directory fallback (its parent is the degenerate root "."), but + // a sibling config.json still names it. + name: "root-level model is named from a root config.json", + setup: func(t *testing.T) string { + dir := t.TempDir() + model(t, dir) + writeFile(t, filepath.Join(dir, "config.json"), `{"_name_or_path":"org/RootModel"}`) + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "RootModel", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + }, + }, + { + // drop: a model directly at `./` with no config.json has no usable + // name — the parent is the degenerate root ".", which yields no + // directory fallback — so no package is emitted. + name: "root-level model with no config.json is dropped", + setup: func(t *testing.T) string { + dir := t.TempDir() + model(t, dir) + return dir + }, + expectedPackages: nil, + }, + { + // rung 1 via parent-walk: findDirHFConfig walks up from the model + // directory, so a config.json in an ancestor names a nested model. + name: "config.json in an ancestor directory names a nested model", + setup: func(t *testing.T) string { + dir := t.TempDir() + model(t, filepath.Join(dir, "dir", "someothertensor")) + writeFile(t, filepath.Join(dir, "dir", "config.json"), `{"_name_or_path":"org/Ancestor"}`) + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "Ancestor", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + }, + }, + { + // grouping: independent models in one scan are grouped by their own + // parent directory and each named from it. + name: "sibling models in one scan are each named by their own directory", + setup: func(t *testing.T) string { + dir := t.TempDir() + model(t, filepath.Join(dir, "sometensor")) + model(t, filepath.Join(dir, "dir", "someothertensor")) + return dir + }, + expectedPackages: []pkg.Package{ + { + Name: "sometensor", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + { + Name: "someothertensor", + Type: pkg.ModelPkg, + Metadata: wantMetadata(""), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fixtureDir := tt.setup(t) + + pkgtest.NewCatalogTester(). + FromDirectory(t, fixtureDir). + Expects(tt.expectedPackages, tt.expectedRelationships). + IgnoreLocationLayer(). + IgnorePackageFields("FoundBy", "Locations"). + TestCataloger(t, NewSafeTensorsCataloger()) + }) + } +} + +// TestSafeTensorsCataloger_shardedDirectory covers the primary multi-shard shape: +// several `model-0000N-of-0000M.safetensors` files in one directory. The +// cataloger must group the shards into a single package, sum their tensor counts, +// record the shard count, and roll each shard up into Parts. (The OCI multi-shard +// path is covered separately in TestSafeTensorsMergeProcessor.) +func TestSafeTensorsCataloger_shardedDirectory(t *testing.T) { + userMeta := map[string]string{"format": "pt"} + // Two shards with distinct tensors → distinct per-shard metadata hashes, so + // the merge treats them as separate shards (3 tensors total across 2 shards). + shardA := map[string]safeTensorsEntry{ + "layers.0.weight": {DType: "BF16", Shape: []int64{10, 10}, DataOffsets: []int64{0, 200}}, + } + shardB := map[string]safeTensorsEntry{ + "layers.1.weight": {DType: "BF16", Shape: []int64{10, 10}, DataOffsets: []int64{0, 200}}, + "layers.2.weight": {DType: "BF16", Shape: []int64{10, 10}, DataOffsets: []int64{200, 400}}, + } + + dir := t.TempDir() + modelDir := filepath.Join(dir, "llama-sharded") + require.NoError(t, os.MkdirAll(modelDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(modelDir, "model-00001-of-00002.safetensors"), + buildSafeTensorsFile(t, userMeta, shardA), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(modelDir, "model-00002-of-00002.safetensors"), + buildSafeTensorsFile(t, userMeta, shardB), 0o644)) + + pkgtest.NewCatalogTester(). + FromDirectory(t, dir). + ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) { + require.Len(t, pkgs, 1) + got := pkgs[0] + assert.Equal(t, "llama-sharded", got.Name, "a sharded model with no config.json is named by its directory") + md := got.Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, 2, md.ShardCount) + assert.Equal(t, uint64(3), md.TensorCount, "tensor counts are summed across shards") + assert.Len(t, md.Parts, 2, "each shard is rolled up into Parts") + assert.Equal(t, "BF16", md.Quantization) + }). + TestCataloger(t, NewSafeTensorsCataloger()) +} + +// TestParseSafeTensorsOCIConfig covers the parser in isolation: it should emit +// a nameless package mirroring the config blob's producer-declared fields, and +// emit nothing for non-safetensors formats so the GGUF cataloger can claim the +// artifact. Naming and license resolution happen in the merge processor and are +// tested separately under TestSafeTensorsMergeProcessor. +func TestParseSafeTensorsOCIConfig(t *testing.T) { + tests := []struct { + name string + blob string + expectedPackages []pkg.Package // nil => parser must emit nothing + }{ + { + name: "emits a nameless package with config-blob fields", + blob: `{"config":{"format":"safetensors","quantization":"Q4_K_M","parameters":"8B","size":"16.00GB","safetensors":{"tensor_count":291}}}`, + expectedPackages: []pkg.Package{ + { + // nameless: the merge processor assigns the name and resolves + // licenses. Config blobs carry no header content, so + // MetadataHash stays empty. The "parameters" label in the blob is + // intentionally ignored: the true count is measured from the + // SafeTensors layer headers, so Parameters stays zero here. + Type: pkg.ModelPkg, + Metadata: pkg.SafeTensorsModelInfo{ + Format: "safetensors", + Quantization: "Q4_K_M", + TotalSize: "16.00GB", + TensorCount: 291, + }, + }, + }, + }, + { + // non-safetensors formats emit nothing so the GGUF cataloger can claim + // the artifact. + name: "ignores non-safetensors format", + blob: `{"config":{"format":"gguf","quantization":"Q4_K_M"}}`, + expectedPackages: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkgtest.NewCatalogTester(). + FromString("/config.json", tt.blob). + Expects(tt.expectedPackages, nil). + IgnorePackageFields("FoundBy", "Locations"). + TestParser(t, parseSafeTensorsOCIConfig) + }) + } +} + +// TestSafeTensorsMergeProcessor exercises the merge processor directly with +// synthetic input. The full-cataloger integration tests cover the realistic +// happy paths; this focuses on grouping, the naming precedence chain, the +// drop-when-unnameable rule, and cross-shard rollup. +func TestSafeTensorsMergeProcessor(t *testing.T) { + dirPkg := func(realPath string, md pkg.SafeTensorsModelInfo) pkg.Package { + return pkg.Package{ + Type: pkg.ModelPkg, + Metadata: md, + Locations: file.NewLocationSet( + file.NewLocation(realPath). + WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + } + } + ociPkg := func(md pkg.SafeTensorsModelInfo) pkg.Package { + return pkg.Package{ + Type: pkg.ModelPkg, + Metadata: md, + Locations: file.NewLocationSet( + file.NewLocation("/"). + WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + } + } + + t.Run("dir scan: parent directory base name names the group when no config.json is present", func(t *testing.T) { + // Without a config.json the dir-scan path falls through to the + // parent directory base name. hugginface style model dir is named after the + // model, so "/models/tiny-llama/weights.safetensors" → "tiny-llama". + p := dirPkg("/models/tiny-llama/weights.safetensors", pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: 4, + Quantization: "BF16", + MetadataHash: "abc", + }) + resolver := file.NewMockResolverForPaths() // no config.json / README available + out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, nil) + require.NoError(t, err) + require.Len(t, out, 1) + assert.Equal(t, "tiny-llama", out[0].Name, "rung 2: parent directory base name") + }) + + t.Run("dir scan: nested model dirs group and name by immediate parent", func(t *testing.T) { + top := dirPkg("/namea/1.safetensors", pkg.SafeTensorsModelInfo{ + Format: "safetensors", TensorCount: 1, MetadataHash: "aaaa", + }) + nested := dirPkg("/namea/nameb/2.safetensors", pkg.SafeTensorsModelInfo{ + Format: "safetensors", TensorCount: 1, MetadataHash: "bbbb", + }) + resolver := file.NewMockResolverForPaths() + out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{top, nested}, nil, nil) + require.NoError(t, err) + require.Len(t, out, 2) + names := []string{out[0].Name, out[1].Name} + assert.ElementsMatch(t, []string{"namea", "nameb"}, names) + }) + + t.Run("dir scan: config.json _name_or_path beats the parent directory fallback", func(t *testing.T) { + // When a sibling config.json carries _name_or_path + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), + []byte(`{"_name_or_path":"org/preferred-name"}`), 0o644)) + stPath := filepath.Join(dir, "weights.safetensors") + p := dirPkg(stPath, pkg.SafeTensorsModelInfo{ + Format: "safetensors", TensorCount: 1, MetadataHash: "abc", + }) + resolver := file.NewMockResolverForPaths(filepath.Join(dir, "config.json")) + out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, nil) + require.NoError(t, err) + require.Len(t, out, 1) + assert.Equal(t, "preferred-name", out[0].Name, "rung 1 (config.json) wins over rung 2 (parent dir)") + }) + + t.Run("OCI: dropped when no name source is available", func(t *testing.T) { + // The vllm-style shape: config-blob package + a weight-layer package, + // both at virtual path "/", no model.file companions on the resolver + // AND no image ref. With nothing to derive a name from, the package is + // dropped + configMd := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: 5, + TotalSize: "1GB", + } + shardMd := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: 5, + Quantization: "BF16", + MetadataHash: "deadbeef", + } + resolver := file.NewMockResolverForMediaTypes(nil) + out, _, err := safeTensorsMergeProcessor( + context.Background(), resolver, + []pkg.Package{ociPkg(configMd), ociPkg(shardMd)}, nil, nil, + ) + require.NoError(t, err) + assert.Empty(t, out, "OCI group with no naming source must be dropped") + }) + + t.Run("OCI: image-ref last segment names the group when config.json is absent", func(t *testing.T) { + // vllm-style artifact: a repacked model whose embedded config.json has + // been stripped of _name_or_path. + configMd := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: 290, + TotalSize: "723MB", + } + shardMd := pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TensorCount: 290, + Quantization: "BF16", + MetadataHash: "deadbeef", + } + resolver := file.NewMockResolverForOCIArtifact( + "docker.io/ai/smollm2-vllm:360M", nil, + ) + out, _, err := safeTensorsMergeProcessor( + context.Background(), resolver, + []pkg.Package{ociPkg(configMd), ociPkg(shardMd)}, nil, nil, + ) + require.NoError(t, err) + require.Len(t, out, 1) + assert.Equal(t, "smollm2-vllm", out[0].Name, "rung 2: image-ref repository basename") + }) + + t.Run("OCI: multi-shard rollup hashes are stable and sorted", func(t *testing.T) { + dir := t.TempDir() + hfConfigPath := filepath.Join(dir, "config.json") + require.NoError(t, os.WriteFile(hfConfigPath, + []byte(`{"architectures":["X"],"_name_or_path":"org/multi"}`), 0o644)) + resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{ + dockerAIModelFileMediaType: {file.NewLocation(hfConfigPath)}, + }) + + configMd := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 9, TotalSize: "3GB"} + shard := func(hash string, cnt uint64) pkg.SafeTensorsModelInfo { + return pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: cnt, Quantization: "BF16", MetadataHash: hash} + } + in := []pkg.Package{ + ociPkg(configMd), + ociPkg(shard("cccc", 3)), + ociPkg(shard("aaaa", 3)), + ociPkg(shard("bbbb", 3)), + } + out1, _, err := safeTensorsMergeProcessor(context.Background(), resolver, in, nil, nil) + require.NoError(t, err) + require.Len(t, out1, 1) + md1 := out1[0].Metadata.(pkg.SafeTensorsModelInfo) + require.Len(t, md1.Parts, 3) + // Parts deterministically sorted by MetadataHash. + assert.Equal(t, + []string{"aaaa", "bbbb", "cccc"}, + []string{md1.Parts[0].MetadataHash, md1.Parts[1].MetadataHash, md1.Parts[2].MetadataHash}, + ) + // Rollup hash is stable across input ordering. + shuffled := []pkg.Package{ociPkg(shard("bbbb", 3)), ociPkg(configMd), ociPkg(shard("aaaa", 3)), ociPkg(shard("cccc", 3))} + out2, _, err := safeTensorsMergeProcessor(context.Background(), resolver, shuffled, nil, nil) + require.NoError(t, err) + md2 := out2[0].Metadata.(pkg.SafeTensorsModelInfo) + assert.Equal(t, md1.MetadataHash, md2.MetadataHash, "rollup hash must not depend on input order") + }) + + t.Run("OCI: license layer SPDX comes from choosealicense frontmatter", func(t *testing.T) { + // The license layer's content carries a choosealicense.com-style YAML + // frontmatter block. The processor should prefer the cheap spdx-id read + // over invoking the full license scanner. + dir := t.TempDir() + licensePath := filepath.Join(dir, "LICENSE") + require.NoError(t, os.WriteFile(licensePath, []byte(`--- +title: Apache License 2.0 +spdx-id: Apache-2.0 +--- + + Apache License + Version 2.0, January 2004 +`), 0o644)) + hfConfigPath := filepath.Join(dir, "config.json") + require.NoError(t, os.WriteFile(hfConfigPath, + []byte(`{"_name_or_path":"org/with-license-fm"}`), 0o644)) + resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{ + dockerAIModelFileMediaType: {file.NewLocation(hfConfigPath)}, + dockerAILicenseMediaType: {file.NewLocation(licensePath)}, + }) + + configMd := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 1} + out, _, err := safeTensorsMergeProcessor( + context.Background(), resolver, + []pkg.Package{ociPkg(configMd)}, nil, nil, + ) + require.NoError(t, err) + require.Len(t, out, 1) + assert.Equal(t, "with-license-fm", out[0].Name) + assertHasLicense(t, out[0], "Apache-2.0") + }) + + t.Run("OCI: license layer wins over a README model-card license", func(t *testing.T) { + // When both a dedicated license layer and a README model-card license are + // present, the producer-curated license layer is authoritative. (If the + // README won, the resolved license would be MIT and this assertion fails.) + dir := t.TempDir() + licensePath := filepath.Join(dir, "LICENSE") + require.NoError(t, os.WriteFile(licensePath, []byte("---\nspdx-id: Apache-2.0\n---\n"), 0o644)) + readmePath := filepath.Join(dir, "README.md") + require.NoError(t, os.WriteFile(readmePath, + []byte("---\nlicense: MIT\nbase_model:\n - org/base\n---\n# Card\n"), 0o644)) + configPath := filepath.Join(dir, "config.json") + require.NoError(t, os.WriteFile(configPath, []byte(`{"_name_or_path":"org/precedence-model"}`), 0o644)) + resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{ + dockerAIModelFileMediaType: {file.NewLocation(configPath), file.NewLocation(readmePath)}, + dockerAILicenseMediaType: {file.NewLocation(licensePath)}, + }) + + configMd := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 1} + out, _, err := safeTensorsMergeProcessor( + context.Background(), resolver, + []pkg.Package{ociPkg(configMd)}, nil, nil, + ) + require.NoError(t, err) + require.Len(t, out, 1) + assert.Equal(t, "precedence-model", out[0].Name) + assertHasLicense(t, out[0], "Apache-2.0") + }) + + t.Run("names packages and still propagates an upstream error", func(t *testing.T) { + // A non-nil upstream error (e.g. another file in the run failed to parse) + // must NOT short-circuit naming. The package is still nameless at this + // point; skipping the merge would let it flow downstream where missing-name + // compliance silently drops it, turning one bad file into the loss of every + // valid model. So we name/drop as usual and pass the error through. + sentinel := assert.AnError + p := dirPkg("/models/x/y.safetensors", pkg.SafeTensorsModelInfo{Format: "safetensors", MetadataHash: "h"}) + resolver := file.NewMockResolverForPaths() // no config.json -> parent directory base name + out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, sentinel) + assert.Equal(t, sentinel, err, "the upstream error is still propagated") + require.Len(t, out, 1) + assert.Equal(t, "x", out[0].Name, "the model is still named (from its parent directory) despite the error") + }) +} + +// TestSafeTensorsNamingPrecedence codifies pickSafeTensorsName's two-rung +// precedence chain. Each case sets the inputs that should activate one rung +// (or neither, asserting the drop path). +// +// Precedence (highest → lowest): +// 1. config.json _name_or_path (path.Base applied; both dir-scan and OCI) +// 2. fallback name — OCI image-ref last segment, or dir-scan parent directory +// base name (the merge processor computes the right one per group) +// → drop (empty name) when nothing matches +func TestSafeTensorsNamingPrecedence(t *testing.T) { + tests := []struct { + name string + nameOrPath string + fallbackName string + want string + }{ + // rung 1 + { + name: "rung 1: _name_or_path beats the fallback", + nameOrPath: "org/MyModel", + fallbackName: "fallback-name", + want: "MyModel", + }, + { + name: "rung 1: applies path.Base to the raw value", + nameOrPath: "very/deep/checkpoint/path/leaf-model", + want: "leaf-model", + }, + { + name: "rung 1: bare name without slashes is preserved", + nameOrPath: "OciModel", + want: "OciModel", + }, + + // rung 2 + { + name: "rung 2: OCI image-ref last segment used when _name_or_path is empty", + fallbackName: "smollm2-vllm", + want: "smollm2-vllm", + }, + { + name: "rung 2: dir-scan parent directory name used when _name_or_path is empty", + fallbackName: "tiny-llama", + want: "tiny-llama", + }, + + // drops + { + name: "drop: both rungs empty", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pickSafeTensorsName(tt.nameOrPath, tt.fallbackName) + assert.Equal(t, tt.want, got) + }) + } +} + +// TestSafeTensorsDirName covers the directory-scan fallback name derivation, +// including the degenerate roots that must yield no name. +func TestSafeTensorsDirName(t *testing.T) { + tests := []struct { + groupKey string + want string + }{ + {groupKey: "/models/tiny-llama", want: "tiny-llama"}, + {groupKey: "/namea", want: "namea"}, + {groupKey: "/namea/nameb", want: "nameb"}, + {groupKey: "/", want: ""}, + {groupKey: ".", want: ""}, + {groupKey: "", want: ""}, + } + for _, tt := range tests { + t.Run(tt.groupKey, func(t *testing.T) { + assert.Equal(t, tt.want, safeTensorsDirName(tt.groupKey)) + }) + } +} + +func TestParseSafeTensorsOCILayer(t *testing.T) { + tensors := map[string]safeTensorsEntry{ + "layer.0.weight": {DType: "BF16", Shape: []int64{1024, 16}, DataOffsets: []int64{0, 32768}}, + "layer.1.weight": {DType: "BF16", Shape: []int64{16, 16}, DataOffsets: []int64{32768, 33280}}, + } + userMeta := map[string]string{"format": "pt"} + wantUserMetadata := pkg.KeyValues{{Key: "format", Value: "pt"}} + blob := buildSafeTensorsFile(t, userMeta, tensors) + wantHash := (&safeTensorsHeader{metadata: userMeta, tensors: tensors}).metadataHash() + + t.Run("emits a nameless package with header-derived metadata", func(t *testing.T) { + // nameless: the merge processor assigns the name. Parameters is the + // summed element count of the two tensors (1024*16 + 16*16 = 16640). + expected := []pkg.Package{ + { + Type: pkg.ModelPkg, + Metadata: pkg.SafeTensorsModelInfo{ + Format: "safetensors", + Parameters: 16640, + Quantization: "BF16", + TensorCount: 2, + UserMetadata: wantUserMetadata, + MetadataHash: wantHash, + }, + }, + } + pkgtest.NewCatalogTester(). + FromString("/", string(blob)). + Expects(expected, nil). + IgnorePackageFields("FoundBy", "Locations"). + TestParser(t, parseSafeTensorsOCILayer) + }) + + t.Run("merged via processor: aggregate fields preserved, hash lifted from single shard", func(t *testing.T) { + // Synthesize the OCI single-shard shape: a config-blob-derived nameless + // package + the weight-layer parser's nameless package (both at virtual + // path "/"). With a companion HF config.json on the resolver to provide + // _name_or_path, the merge processor produces a single named model. + dir := t.TempDir() + hfConfigPath := filepath.Join(dir, "config.json") + require.NoError(t, os.WriteFile(hfConfigPath, + []byte(`{"architectures":["Qwen3ForCausalLM"],"_name_or_path":"org/qwen-test"}`), 0o644)) + resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{ + dockerAIModelFileMediaType: {file.NewLocation(hfConfigPath)}, + }) + + configPkg := pkg.Package{ + Type: pkg.ModelPkg, + Metadata: pkg.SafeTensorsModelInfo{ + Format: "safetensors", + TotalSize: "5.00GB", + Quantization: "Q4_K_M", // raw producer string + TensorCount: 9999, + }, + Locations: file.NewLocationSet( + file.NewLocation("/").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + } + reader := file.NewLocationReadCloser( + file.NewLocation("/").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + io.NopCloser(bytes.NewReader(blob)), + ) + layerPkgs, _, err := parseSafeTensorsOCILayer(context.Background(), nil, nil, reader) + require.NoError(t, err) + require.Len(t, layerPkgs, 1) + + out, _, err := safeTensorsMergeProcessor( + context.Background(), resolver, + append([]pkg.Package{configPkg}, layerPkgs...), nil, nil, + ) + require.NoError(t, err) + require.Len(t, out, 1) + + got := out[0] + assert.Equal(t, "qwen-test", got.Name, "name comes from the companion config.json _name_or_path") + md := got.Metadata.(pkg.SafeTensorsModelInfo) + // Aggregate-declared TensorCount/TotalSize win as authoritative totals; the + // per-shard TensorCount must NOT be summed into the aggregate. Parameters is + // the exception: it is always measured from the shard layer header (here the + // 16640-element blob), never taken from the config aggregate. + assert.Equal(t, uint64(9999), md.TensorCount) + assert.Equal(t, "5.00GB", md.TotalSize) + assert.Equal(t, uint64(16640), md.Parameters) + // Aggregate Quantization wins when set; shard's normalized dtype is the + // fallback (not exercised here because the config had Q4_K_M). + assert.Equal(t, "Q4_K_M", md.Quantization) + // Architecture comes from companion HF config.json enrichment. + assert.Equal(t, "Qwen3ForCausalLM", md.Architecture) + // Single-shard groups skip Parts; the rollup hash is the lone shard's hash. + assert.Nil(t, md.Parts) + assert.Equal(t, wantHash, md.MetadataHash) + assert.Equal(t, wantUserMetadata, md.UserMetadata) + assert.Equal(t, 1, md.ShardCount) + }) +} + +// TestParseSafeTensorsOCILayer_realFixture grounds the OCI layer parser +// against a real `[prefix + JSON header]` captured from a public Docker AI +// model artifact (docker.io/ai/nomic-embed-text-v2-moe-safetensors:475M). +// The fixture and the tool that produced it live in +// testdata/safetensors/; see the README there to refresh. +// +// Locking in the field values guards against changes to the header parser +// silently breaking on real-world content shape. +func TestParseSafeTensorsOCILayer_realFixture(t *testing.T) { + // nameless before the merge processor runs. The fixture is immutable on disk; + // the locked field values (notably MetadataHash) guard against the header + // parser silently breaking on real-world content shape — if MetadataHash + // changes, either the hash algorithm or the canonicalization changed, both of + // which callers may rely on for cross-source identity. + expected := []pkg.Package{ + { + Type: pkg.ModelPkg, + Metadata: pkg.SafeTensorsModelInfo{ + Format: "safetensors", + Parameters: 475292928, // exact element count summed across the 148 tensors in this shard + Quantization: "F32", // every tensor in the captured shard is F32 + TensorCount: 148, // nomic-embed-v2-moe 475M ships 148 tensor entries in this shard + UserMetadata: pkg.KeyValues{{Key: "format", Value: "pt"}}, + MetadataHash: "6026c28a883ab918", + }, + }, + } + + pkgtest.NewCatalogTester(). + FromFile(t, filepath.Join("testdata", "safetensors", "nomic-embed-475M.header.safetensors")). + Expects(expected, nil). + IgnorePackageFields("FoundBy", "Locations"). + TestParser(t, parseSafeTensorsOCILayer) +} + +func TestSafeTensorsCrossSourceHashParity(t *testing.T) { + // Same content, two paths: a directory scan via parseSafeTensorsFile, and an + // OCI weight-layer scan via parseSafeTensorsOCILayer. The MetadataHash of + // the dir-scan package must equal the per-shard hash captured before the + // merge processor absorbs it. This is the convergence point that lets a + // caller correlate the two source types. + tensors := map[string]safeTensorsEntry{ + "a.weight": {DType: "BF16", Shape: []int64{8, 8}, DataOffsets: []int64{0, 128}}, + "b.weight": {DType: "BF16", Shape: []int64{4, 4}, DataOffsets: []int64{128, 160}}, + } + userMeta := map[string]string{"format": "pt", "producer": "test"} + blob := buildSafeTensorsFile(t, userMeta, tensors) + + // dir-scan path + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "model.safetensors"), blob, 0o644)) + dirReader := func() file.LocationReadCloser { + f, err := os.Open(filepath.Join(dir, "model.safetensors")) + require.NoError(t, err) + return file.NewLocationReadCloser(file.NewLocation(filepath.Join(dir, "model.safetensors")), f) + }() + dirPkgs, _, err := parseSafeTensorsFile(context.Background(), nil, nil, dirReader) + require.NoError(t, err) + require.Len(t, dirPkgs, 1) + dirHash := dirPkgs[0].Metadata.(pkg.SafeTensorsModelInfo).MetadataHash + require.NotEmpty(t, dirHash) + + // OCI weight-layer path + ociReader := file.NewLocationReadCloser(file.NewLocation("/"), io.NopCloser(bytes.NewReader(blob))) + ociPkgs, _, err := parseSafeTensorsOCILayer(context.Background(), nil, nil, ociReader) + require.NoError(t, err) + require.Len(t, ociPkgs, 1) + ociHash := ociPkgs[0].Metadata.(pkg.SafeTensorsModelInfo).MetadataHash + + assert.Equal(t, dirHash, ociHash, "same content via dir scan and OCI weight-layer scan must hash equal") +} + +func assertHasLicense(t *testing.T, p pkg.Package, value string) { + t.Helper() + for _, l := range p.Licenses.ToSlice() { + if l.Value == value { + return + } + } + t.Errorf("expected license %q, got %+v", value, p.Licenses.ToSlice()) +} + +func TestReadSafeTensorsHeader(t *testing.T) { + zeroLength := make([]byte, 8) // length prefix of 0 + + truncatedBody := make([]byte, 8) + binary.LittleEndian.PutUint64(truncatedBody, 100) // claims 100 bytes but supplies none + + // length prefix declares a header larger than the OOM guard allows; the body + // is intentionally omitted because the cap must reject before any allocation. + overCap := make([]byte, 8) + binary.LittleEndian.PutUint64(overCap, uint64(maxSafeTensorsHeaderSize)+1) + + malformedJSON := make([]byte, 8+len("not-json")) + binary.LittleEndian.PutUint64(malformedJSON[:8], uint64(len("not-json"))) + copy(malformedJSON[8:], "not-json") + + tests := []struct { + name string + data []byte + wantErr bool + assert func(t *testing.T, h *safeTensorsHeader) + }{ + { + name: "valid header", + data: buildSafeTensorsFile(t, map[string]string{"format": "pt"}, map[string]safeTensorsEntry{ + "w": {DType: "F32", Shape: []int64{2, 2}, DataOffsets: []int64{0, 16}}, + }), + assert: func(t *testing.T, h *safeTensorsHeader) { + assert.Len(t, h.tensors, 1) + assert.Equal(t, "pt", h.metadata["format"]) + }, + }, + { + name: "zero-length header", + data: zeroLength, + wantErr: true, + }, + { + name: "truncated body", + data: truncatedBody, + wantErr: true, + }, + { + name: "header exceeds max size cap", + data: overCap, + wantErr: true, + }, + { + name: "malformed JSON body", + data: malformedJSON, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h, err := readSafeTensorsHeader(bytes.NewReader(tt.data)) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.assert(t, h) + }) + } +} + +func TestSafeTensorsHeader_metadataHash(t *testing.T) { + base := &safeTensorsHeader{ + metadata: map[string]string{"format": "pt"}, + tensors: map[string]safeTensorsEntry{ + "a.weight": {DType: "F32", Shape: []int64{2, 2}, DataOffsets: []int64{0, 16}}, + "b.weight": {DType: "F16", Shape: []int64{4}, DataOffsets: []int64{16, 24}}, + }, + } + + // deterministic across calls and independent of map insertion order + reordered := &safeTensorsHeader{ + metadata: map[string]string{"format": "pt"}, + tensors: map[string]safeTensorsEntry{ + "b.weight": {DType: "F16", Shape: []int64{4}, DataOffsets: []int64{16, 24}}, + "a.weight": {DType: "F32", Shape: []int64{2, 2}, DataOffsets: []int64{0, 16}}, + }, + } + assert.Equal(t, base.metadataHash(), reordered.metadataHash()) + assert.Len(t, base.metadataHash(), 16) + + // changing a tensor changes the hash + changed := &safeTensorsHeader{ + metadata: base.metadata, + tensors: map[string]safeTensorsEntry{ + "a.weight": {DType: "F32", Shape: []int64{2, 3}, DataOffsets: []int64{0, 24}}, + "b.weight": {DType: "F16", Shape: []int64{4}, DataOffsets: []int64{24, 32}}, + }, + } + assert.NotEqual(t, base.metadataHash(), changed.metadataHash()) + + // changing __metadata__ changes the hash + differentMeta := &safeTensorsHeader{metadata: map[string]string{"format": "np"}, tensors: base.tensors} + assert.NotEqual(t, base.metadataHash(), differentMeta.metadataHash()) +} + +func TestSafeTensorsHeader_parameterCountAndDType(t *testing.T) { + h := &safeTensorsHeader{tensors: map[string]safeTensorsEntry{ + "big": {DType: "BF16", Shape: []int64{1000, 16}}, + "small": {DType: "F32", Shape: []int64{16, 16}}, + "scalar": {DType: "F32", Shape: []int64{}}, // empty shape contributes 1 + }} + assert.Equal(t, uint64(1000*16+16*16+1), h.parameterCount()) + assert.Equal(t, "BF16", h.dominantDType()) +} + +func TestSafeTensorsHeader_parameterCount_nonPositiveDims(t *testing.T) { + // a zero or negative dim zeroes that tensor's contribution rather than + // wrapping the uint64 product (a negative int64 cast would otherwise become a + // huge value); other tensors are unaffected. + h := &safeTensorsHeader{tensors: map[string]safeTensorsEntry{ + "good": {DType: "F32", Shape: []int64{4, 4}}, + "zero": {DType: "F32", Shape: []int64{4, 0}}, + "negative": {DType: "F32", Shape: []int64{-1, 8}}, + }} + assert.Equal(t, uint64(16), h.parameterCount()) +} + +func TestSafeTensorsHeader_dominantDType_tieBreak(t *testing.T) { + // two dtypes tied on parameter count: the lexicographically smallest wins, + // independent of map iteration order. + h := &safeTensorsHeader{tensors: map[string]safeTensorsEntry{ + "a": {DType: "F32", Shape: []int64{4, 4}}, + "b": {DType: "BF16", Shape: []int64{4, 4}}, + }} + assert.Equal(t, "BF16", h.dominantDType()) +} + +func TestNormalizeDType(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + {name: "already canonical BF16", in: "BF16", want: "BF16"}, + {name: "float16 alias", in: "float16", want: "F16"}, + {name: "FP32 alias", in: "FP32", want: "F32"}, + {name: "int8 alias", in: "int8", want: "I8"}, + {name: "U8 passthrough", in: "U8", want: "U8"}, + {name: "bool", in: "bool", want: "BOOL"}, + {name: "unknown value uppercased", in: "weird", want: "WEIRD"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, normalizeDType(tt.in)) + }) + } +} + +func TestParseFrontmatter(t *testing.T) { + tests := []struct { + name string + input string + wantNil bool + wantLicense string + wantBaseModel []string + }{ + { + name: "list base_model", + input: "---\nlicense: mit\nbase_model:\n - org/Model\n---\nbody", + wantLicense: "mit", + wantBaseModel: []string{"org/Model"}, + }, + { + name: "scalar base_model", + input: "---\nlicense: apache-2.0\nbase_model: org/Model\n---\n", + wantLicense: "apache-2.0", + wantBaseModel: []string{"org/Model"}, + }, + { + name: "leading BOM", + input: "\xef\xbb\xbf---\nlicense: mit\n---\n", + wantLicense: "mit", + }, + { + name: "no frontmatter", + input: "# just a heading\n", + wantNil: true, + }, + { + name: "unterminated frontmatter", + input: "---\nlicense: mit\n", + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fm := parseFrontmatter([]byte(tt.input)) + if tt.wantNil { + assert.Nil(t, fm) + return + } + require.NotNil(t, fm) + assert.Equal(t, tt.wantLicense, fm.License) + if tt.wantBaseModel != nil { + assert.Equal(t, tt.wantBaseModel, fm.BaseModel) + } + }) + } +} + +// TestParseLicenseFrontmatter covers the choosealicense.com-style YAML +// frontmatter Docker Model Runner uses for its license layers. Only spdx-id +// is consumed; everything else in the block is ignored. +func TestParseLicenseFrontmatter(t *testing.T) { + // The Apache-2.0 case is the exact frontmatter shape from + // https://github.com/github/choosealicense.com/blob/gh-pages/_licenses/apache-2.0.txt + // Docker AI license layers ship a near-identical block. + tests := []struct { + name string + input string + want string + }{ + { + name: "Apache-2.0 (the canonical choosealicense.com shape)", + input: `--- +title: Apache License 2.0 +spdx-id: Apache-2.0 +redirect_from: /licenses/apache/ +featured: true +hidden: false + +description: A permissive license whose main conditions require preservation of copyright and license notices. + +how: Create a text file (typically named LICENSE or LICENSE.txt) in the root of your source code and copy the text of the license into the file. + +using: + Kubernetes: https://github.com/kubernetes/kubernetes/blob/master/LICENSE + PDF.js: https://github.com/mozilla/pdf.js/blob/master/LICENSE + Swift: https://github.com/apple/swift/blob/main/LICENSE.txt + +permissions: + - commercial-use + - modifications + - distribution + - patent-use + - private-use + +conditions: + - include-copyright + - document-changes + +limitations: + - trademark-use + - liability + - warranty + +--- + + Apache License + Version 2.0, January 2004 +`, + want: "Apache-2.0", + }, + { + name: "MIT with BOM prefix", + input: "\xef\xbb\xbf---\ntitle: MIT License\nspdx-id: MIT\n---\nThe MIT License...\n", + want: "MIT", + }, + { + name: "frontmatter without spdx-id falls through (returns empty)", + input: "---\ntitle: Something\ndescription: no spdx-id here\n---\nbody\n", + want: "", + }, + { + name: "plain license text without any frontmatter", + input: " Apache License\n Version 2.0, January 2004\n", + want: "", + }, + { + name: "unterminated frontmatter block", + input: "---\nspdx-id: MIT\n(never closes)\n", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, parseLicenseFrontmatter([]byte(tt.input))) + }) + } +} + +func TestDockerAIModelConfigMediaTypes(t *testing.T) { + // supported mirrors how the resolver matches: filepath.Match each registered + // media type against a layer's media type. + supported := func(mt string) bool { + for _, p := range dockerAIModelConfigMediaTypes { + if ok, err := filepath.Match(p, mt); err == nil && ok { + return true + } + } + return false + } + + tests := []struct { + name string + mediaType string + want bool + }{ + // the known, verified schema versions are consumed + {name: "known schema v0.1 is consumed", mediaType: "application/vnd.docker.ai.model.config.v0.1+json", want: true}, + {name: "known schema v0.2 is consumed", mediaType: "application/vnd.docker.ai.model.config.v0.2+json", want: true}, + // unknown/future schema versions are intentionally NOT consumed, to avoid + // silently ingesting a potentially breaking config change + {name: "unknown schema v0.3 is rejected", mediaType: "application/vnd.docker.ai.model.config.v0.3+json", want: false}, + {name: "far-future schema v9.9 is rejected", mediaType: "application/vnd.docker.ai.model.config.v9.9+json", want: false}, + // sibling layer media types are not matched either + {name: "sibling model.file layer is not matched", mediaType: "application/vnd.docker.ai.model.file", want: false}, + {name: "sibling gguf layer is not matched", mediaType: "application/vnd.docker.ai.gguf.v3", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, supported(tt.mediaType)) + }) + } +} diff --git a/syft/pkg/cataloger/ai/processor.go b/syft/pkg/cataloger/ai/processor.go index c3ca8c3e7..fd0e29292 100644 --- a/syft/pkg/cataloger/ai/processor.go +++ b/syft/pkg/cataloger/ai/processor.go @@ -1,59 +1,172 @@ package ai import ( + "context" + "path" + "sort" + + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) -// ggufMergeProcessor consolidates multiple GGUF packages into a single package -// representing the AI model. When scanning OCI images with multiple layers, -// each layer may produce a separate package. This processor finds the package -// with a name and merges metadata from nameless packages into its GGUFFileParts field. -// Only packages with a non-empty name are returned in the final result. -func ggufMergeProcessor(pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { - if err != nil { - return pkgs, rels, err - } - +// safeTensorsMergeProcessor owns naming, license resolution, and final package +// assembly. SafeTensors packages reach it nameless from the parsers; it groups +// them per model, merges the per-shard metadata, resolves a name + licenses, and +// drops any model it cannot name. +func safeTensorsMergeProcessor(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { if len(pkgs) == 0 { return pkgs, rels, err } - // Separate packages with names from those without - var namedPkgs []pkg.Package - var namelessHeaders []pkg.GGUFFileHeader - - for _, p := range pkgs { - if p.Name != "" { - namedPkgs = append(namedPkgs, p) - } else { - if header, ok := p.Metadata.(pkg.GGUFFileHeader); ok { - // We do not want a kv hash for nameless headers - header.MetadataKeyValuesHash = "" - namelessHeaders = append(namelessHeaders, header) - } - } + // Note: we do NOT early-return when err != nil. A non-nil err here means some + // file in the run failed to parse, but the successfully-parsed packages are + // still nameless until this processor names or drops them. Skipping that work + // would let those nameless packages flow downstream, where they are silently + // dropped by missing-name compliance — turning one bad file into the loss of + // every otherwise-valid model. So we always name/drop and propagate err. + stPkgs, other := partitionSafeTensorsPackages(pkgs) + if len(stPkgs) == 0 { + return pkgs, rels, err } - // If there are no named packages, return nothing - if len(namedPkgs) == 0 { - return nil, rels, err + if fromOCIArtifact(stPkgs) { + return append(other, mergeOCIModel(ctx, resolver, stPkgs)...), rels, err } + return append(other, mergeDirModels(ctx, resolver, stPkgs)...), rels, err +} - // merge nameless headers into a single named package; - // if there are multiple named packages, return them without trying to merge headers. - // we cannot determine which nameless headers belong to which package - // this is because the order we receive the gguf headers in is not guaranteed - // to match the layer order in the original oci image - if len(namedPkgs) == 1 && len(namelessHeaders) > 0 { - winner := &namedPkgs[0] - if header, ok := winner.Metadata.(pkg.GGUFFileHeader); ok { - header.Parts = namelessHeaders - winner.Metadata = header +// partitionSafeTensorsPackages separates safetensors packages from anything else +// flowing through the processor. +func partitionSafeTensorsPackages(pkgs []pkg.Package) (safeTensors, other []pkg.Package) { + for _, p := range pkgs { + if _, ok := p.Metadata.(pkg.SafeTensorsModelInfo); ok { + safeTensors = append(safeTensors, p) + continue } + other = append(other, p) } + return safeTensors, other +} - // Largest number of key value +// fromOCIArtifact reports whether the packages came from an OCI model artifact. +// That source (the ContainerImageModel resolver) presents every layer at the +// virtual path "/", whereas a filesystem scan always carries a real file path. A +// single scan is one source, so the first package is representative of the rest. +func fromOCIArtifact(pkgs []pkg.Package) bool { + loc := primaryEvidenceLocation(pkgs[0]) + return loc != nil && loc.RealPath == "/" +} - return namedPkgs, rels, err +// mergeOCIModel treats the whole OCI artifact as a single model: every layer +// merges into one package, named from the artifact's config.json/README or its +// image reference. +func mergeOCIModel(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package) []pkg.Package { + merged := mergeSafeTensorsGroup(pkgs) + + md := merged.Metadata.(pkg.SafeTensorsModelInfo) + id := resolveSafeTensorsOCIIdentity(ctx, resolver, &md) + merged.Metadata = md // write architecture enrichment back before assembly + + if p, ok := assembleSafeTensorsPackage(merged, id); ok { + return []pkg.Package{p} + } + return nil +} + +// mergeDirModels groups filesystem-scanned files by their parent directory and +// emits one model per directory +func mergeDirModels(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package) []pkg.Package { + groups := groupByParentDir(pkgs) + + // deterministic iteration order so the SBOM doesn't depend on map order + dirs := make([]string, 0, len(groups)) + for dir := range groups { + dirs = append(dirs, dir) + } + sort.Strings(dirs) + + var out []pkg.Package + for _, dir := range dirs { + merged := mergeSafeTensorsGroup(groups[dir]) + + md := merged.Metadata.(pkg.SafeTensorsModelInfo) + id := resolveSafeTensorsDirIdentity(ctx, resolver, dir, &md) + merged.Metadata = md // write architecture enrichment back before assembly + + if p, ok := assembleSafeTensorsPackage(merged, id); ok { + out = append(out, p) + } + } + return out +} + +// groupByParentDir buckets filesystem-scanned models by the directory their +// primary-evidence file lives in. + +// This encodes a deliberate assumption: a directory holds one logical +// model, so every .safetensors file in a directory is treated as a shard of the +// same modeland merged into one package. +// The trade-off is that if a directory happens to contain several unrelated models, +// they are merged into one package rather than reported separately. +// We accept that because the conventional on-disk layout gives each model +// (with all of its shards) its own directory. +// We have no reliable per-file signal to tell co-located-but-independent models apart. +func groupByParentDir(pkgs []pkg.Package) map[string][]pkg.Package { + out := make(map[string][]pkg.Package) + for _, p := range pkgs { + loc := primaryEvidenceLocation(p) + if loc == nil { + continue + } + dir := path.Dir(loc.RealPath) + out[dir] = append(out[dir], p) + } + return out +} + +func primaryEvidenceLocation(p pkg.Package) *file.Location { + locs := p.Locations.ToSlice() + for i, l := range locs { + if l.Annotations != nil && l.Annotations[pkg.EvidenceAnnotationKey] == pkg.PrimaryEvidenceAnnotation { + return &locs[i] + } + } + if len(locs) > 0 { + return &locs[0] + } + return nil +} + +// safeTensorsIdentity is the fully-resolved naming/license result for a model. +// Each source resolver (dir or OCI) populates it so assembly stays source-agnostic. +type safeTensorsIdentity struct { + nameOrPath string + fallbackName string + licenses []pkg.License + supporting []file.Location +} + +// assembleSafeTensorsPackage finalizes a merged model from its resolved identity: +// it picks the name, attaches licenses and supporting evidence, and sets the ID. +// A model with no name source is dropped (ok=false). +func assembleSafeTensorsPackage(merged pkg.Package, id safeTensorsIdentity) (pkg.Package, bool) { + name := pickSafeTensorsName(id.nameOrPath, id.fallbackName) + if name == "" { + log.Debugf("dropped safetensors model package (metadata hash %q): no name source", + merged.Metadata.(pkg.SafeTensorsModelInfo).MetadataHash) + return pkg.Package{}, false + } + + if len(id.licenses) > 0 { + merged.Licenses = pkg.NewLicenseSet(id.licenses...) + } + for _, loc := range id.supporting { + merged.Locations.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)) + } + + merged.Name = name + merged.SetID() + return merged, true } diff --git a/syft/pkg/cataloger/ai/testdata/safetensors/README.md b/syft/pkg/cataloger/ai/testdata/safetensors/README.md new file mode 100644 index 000000000..eecbe0577 --- /dev/null +++ b/syft/pkg/cataloger/ai/testdata/safetensors/README.md @@ -0,0 +1,28 @@ +# SafeTensors header fixtures + +These fixtures are `[8-byte length prefix + JSON header]` captures from +public Docker AI model artifacts on the registry. + +`extract_header.go` does a range-GET of the first several MB of the layer, +slices off just `[prefix + JSON header]`, and writes +that to disk. + +## Refreshing a fixture + +```sh +# from the package root +go run ./testdata/safetensors/extract_header.go \ + docker.io/ai/nomic-embed-text-v2-moe-safetensors:475M \ + ./testdata/safetensors/nomic-embed-475M.header.safetensors +``` + +The tool prints the layer digest it selected and the number of top-level keys +in the captured header. If you see `header length N does not fit in M fetched +bytes`, raise `fetchBytes` in `extract_header.go` and rerun. + +## Notes + +- Pick one shard, not the full sharded set. The fixture is meant to exercise + the per-shard parser; merging across shards has its own tests. +- Don't commit anything larger than ~1 MB. If a model has an unusually large + header, capture a smaller model instead. diff --git a/syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go b/syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go new file mode 100644 index 000000000..4f7d72a60 --- /dev/null +++ b/syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go @@ -0,0 +1,149 @@ +// extract_header is a manual fixture tool that captures the real on-disk +// safetensors header from a Docker AI OCI model artifact (a vnd.docker.ai.safetensors +// layer) and writes just [8-byte length prefix + JSON header] to a destination +// file. Tensor data following the header is never downloaded, so the resulting +// fixture is a few KB to a few MB even for multi-GB models. +// +// This file lives under testdata/ so the Go build system ignores it. Run it +// manually when refreshing fixtures: +// +// go run ./testdata/safetensors/extract_header.go \ +// docker.io/ai/nomic-embed-text-v2-moe-safetensors:475M \ +// ./testdata/safetensors/nomic-embed-475M.header.safetensors +package main + +import ( + "context" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +const ( + safetensorsLayerMediaType = "application/vnd.docker.ai.safetensors" + // 8 MB matches maxHeaderBytes in the OCI model source. Real model headers + // are well under 1 MB; the extra slack covers outliers. + fetchBytes = 8 * 1024 * 1024 +) + +func main() { + if len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) + os.Exit(2) + } + if err := run(os.Args[1], os.Args[2]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(refStr, outPath string) error { + ctx := context.Background() + ref, err := name.ParseReference(refStr) + if err != nil { + return fmt.Errorf("parse reference: %w", err) + } + + opts := []remote.Option{ + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithContext(ctx), + } + + desc, err := remote.Get(ref, opts...) + if err != nil { + return fmt.Errorf("fetch descriptor: %w", err) + } + + manifest := &v1.Manifest{} + if err := json.Unmarshal(desc.Manifest, manifest); err != nil { + return fmt.Errorf("decode manifest: %w", err) + } + + weightLayer := pickWeightLayer(manifest) + if weightLayer == nil { + return fmt.Errorf("no %q layer found in %s", safetensorsLayerMediaType, ref) + } + fmt.Fprintf(os.Stderr, "selected layer %s (%d bytes on-disk)\n", weightLayer.Digest, weightLayer.Size) + + prefix, err := fetchPrefix(ctx, ref, weightLayer.Digest, opts) + if err != nil { + return fmt.Errorf("fetch layer prefix: %w", err) + } + + header, err := sliceHeader(prefix) + if err != nil { + return fmt.Errorf("extract header: %w", err) + } + + if err := os.WriteFile(outPath, header, 0o644); err != nil { + return fmt.Errorf("write fixture: %w", err) + } + fmt.Fprintf(os.Stderr, "wrote %d bytes to %s\n", len(header), outPath) + return nil +} + +// pickWeightLayer returns the first vnd.docker.ai.safetensors layer in the +// manifest, or nil if none exists. For sharded models we deliberately only +// capture one shard: the fixture is meant to exercise the parser, not the +// merge step. +func pickWeightLayer(manifest *v1.Manifest) *v1.Descriptor { + for i := range manifest.Layers { + if string(manifest.Layers[i].MediaType) == safetensorsLayerMediaType { + return &manifest.Layers[i] + } + } + return nil +} + +// fetchPrefix range-reads the first fetchBytes of a layer. Closing the reader +// terminates the underlying HTTP body, so we never download the tensor data +// that follows the header. +func fetchPrefix(_ context.Context, ref name.Reference, digest v1.Hash, opts []remote.Option) ([]byte, error) { + layer, err := remote.Layer(ref.Context().Digest(digest.String()), opts...) + if err != nil { + return nil, err + } + reader, err := layer.Compressed() + if err != nil { + return nil, err + } + defer reader.Close() + + buf := make([]byte, fetchBytes) + n, err := io.ReadFull(reader, buf) + if err != nil && err != io.ErrUnexpectedEOF { + return nil, err + } + return buf[:n], nil +} + +// sliceHeader reads the 8-byte little-endian length prefix and returns just +// [prefix + JSON header]. It also probes the JSON to make sure the captured +// fixture is well-formed, so we never commit a half-truncated header. +func sliceHeader(buf []byte) ([]byte, error) { + if len(buf) < 8 { + return nil, fmt.Errorf("short read: only %d bytes", len(buf)) + } + headerLen := binary.LittleEndian.Uint64(buf[:8]) + if headerLen == 0 { + return nil, fmt.Errorf("header length is zero") + } + if headerLen > uint64(len(buf)-8) { + return nil, fmt.Errorf("header length %d does not fit in %d fetched bytes; increase fetchBytes", headerLen, len(buf)) + } + + out := buf[:8+int(headerLen)] + var probe map[string]json.RawMessage + if err := json.Unmarshal(out[8:], &probe); err != nil { + return nil, fmt.Errorf("captured JSON does not parse: %w", err) + } + fmt.Fprintf(os.Stderr, "header parses cleanly: %d top-level keys\n", len(probe)) + return out, nil +} diff --git a/syft/pkg/cataloger/ai/testdata/safetensors/nomic-embed-475M.header.safetensors b/syft/pkg/cataloger/ai/testdata/safetensors/nomic-embed-475M.header.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..8196899047b5b36b67712a58a4a5ad76a280aed8 GIT binary patch literal 15520 zcmb`O+iqMn6oz@0x!xN2d_Y|D3S1x*rOl)*v`I(?3PODk9+!3S_Dm+r+rbW3rK$dJ zmJh%G<#GP~;XC^8pWW5fm&5V;=K6Sjb+!BcpWW^K?)%-tarbT69lpG~`h2&4 z_v!ixe|mHL`{4jT{Lz}-vU~b?jsN#^4C$9;M;iCHw@-&7{M(<^VpTN1oHV{3K7IHI z%Ku9RHI|^AD?*2xn@@Kip7zK4-wt;2=+|B*Ld zDVJmh%HvP851|ap-TV8S!{h$*_1}lbr@h)=ACGtYpML-C&(3fgtK7@+uG&NiKBYwE zv?;yOLXKPvvWT$6=#8^6^3aK_`#+9X50Cf1cB&F*rphxYB)3si1@#I8RHH7e1|fip zLDhA4S=X1(5BuBqx>FIY{}=nBDicE|XL*yf7`hcpH+2GU<*ipu*?7Pw3@ooZyn zD$Ix!8jFqx%i~t`{g3ahzGs8kqn)=I8<3F;MwTSi>aghL46psW}~qtScZn}@$14v)vD z5eul|gb6Cb;e?0&*FAGcMN&jeXg(#|K{SQ#&HW(>DNmh3|Y4;2p} z9>hmj9P4@Bb-Ko+RSNNvGDaTFVVgq$$uhv2EHA085TOW$-z1 zldh7;bUE8)U-!7e`nUa+j zGs=n5E2tgrh(!Qk1#t8z_h2h)a67Ha46#b?!cLH0K{!LL!2=cnfEAq%5#~wg_{@yY zt-6eeDl}FBTj?u`k!%hiq+B32h|MwAF*q%VK;41}7&Bfr$17r@4pc4>Xon;#wbLet z#h^HIQdVLlKIilb+A=wy5dvsQn-Jt1+CrN%xmBN0*qKWW*d?l0+=j^kkSGYW!DK7i zkDa}&JE=3s8Y^~6&p{JGkAOs3wDDORXb0q>M96v#Nx;FNS1F5zb}g7_#V(2~e3P74 zF--J2i~Wb4us=kQZb7&#UVE;HS1d%q{s@A~1(M*)Ou?HNGG>2-KwU-zIAvvLn#p(w zA>{&r`ylB!ag>< ziRu-%VR8sW3IgGoiHBBQI9~m9#;fWDl;kfFpMz#H9s!ZEz(Xp0+93B$0&p zHMpBrWr8@Erp5w91HXMg^EEA2!2n=+xQEE5v8mXBwl7*?B?vuc)nx<}*H{HpCI=s$ zlncbm&&R?<#=|-s>J~(DjcJ)OIfS4xLohH%P8z384mc#v9C%(OX5w>BZ`*8{9MA}X zw3Ka}f-B9s z#P0~Wd|!;ObvxLWq(z_pn{woE>XSW+V)34qyW%pc*z9UV8ZfFa~ZEn zGX=&^j43Bdub_>Lhb#gB3%-+q4oz&zR@UHdT9peV%1zk`(klpOsBw=+RUiOZVCT`V zzohm2WmcLoxmC9y;0d+73T84MeRx!62z-Cmm>3RVBk2JVq+1Zqh);%is9qJkC9_5d zDi;VuFDc{YX_G?^iZcf;X++yO=k!+DmdOE)5J-dHcyjVpZlTSY+^SztQ4S`Ts9td! zCWk8Tihk@oP5%aPS_lQWp5m6u$O> zW=Fb&VBni%yozC>hXYt1Q}(72+du^AG6K3AvFXUgVPCNj1=~OfDHjO*W{Ny= 0 { + li, err := storeConfigBlobAsLayer(artifact, tempDir) + if err != nil { + cleanup() + return "", nil, err + } + layerFiles[artifact.Manifest.Config.Digest.String()] = li + } + + // Companion layers (README, config.json, tokenizer.json, LICENSE). Small by + // convention; fetched in full up to maxCompanionBytes. + if artifact.Format == modelFormatSafeTensors { + for _, layer := range artifact.CompanionLayers { + li, err := fetchCompanionLayer(ctx, client, artifact.Reference, layer, tempDir) + if err != nil { + cleanup() + return "", nil, err + } + layerFiles[layer.Digest.String()] = li + } + } + + // SafeTensors weight-layer headers. We only pull the leading prefix (same + // budget as a GGUF header) + if artifact.Format == modelFormatSafeTensors { + for _, layer := range artifact.SafeTensorsLayers { + li, err := fetchSafeTensorsLayerHeader(ctx, client, artifact.Reference, layer, tempDir) + if err != nil { + cleanup() + return "", nil, err + } + layerFiles[layer.Digest.String()] = li + } + } + + resolver := fileresolver.NewContainerImageModel( + tempDir, + layerFiles, + artifact.Reference.String(), + ) return tempDir, resolver, nil } -// fetchSingleGGUFHeader fetches a single GGUF layer header and writes it to a temp file. +// storeConfigBlobAsLayer writes the already-fetched raw config bytes to a temp +// file so the resolver can serve them via media type +func storeConfigBlobAsLayer(artifact *modelArtifact, tempDir string) (fileresolver.LayerInfo, error) { + digest := artifact.Manifest.Config.Digest.String() + safeDigest := strings.ReplaceAll(digest, ":", "-") + tempPath := filepath.Join(tempDir, safeDigest+".config.json") + if err := os.WriteFile(tempPath, artifact.RawConfig, 0600); err != nil { + return fileresolver.LayerInfo{}, fmt.Errorf("failed to write config blob: %w", err) + } + return fileresolver.LayerInfo{ + TempPath: tempPath, + MediaType: string(artifact.Manifest.Config.MediaType), + }, nil +} + +// fetchCompanionLayer downloads a companion (non-weight) layer to a temp file +func fetchCompanionLayer(ctx context.Context, client *registryClient, ref name.Reference, layer v1.Descriptor, tempDir string) (fileresolver.LayerInfo, error) { + data, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxCompanionBytes) + if err != nil { + return fileresolver.LayerInfo{}, fmt.Errorf("failed to fetch companion layer: %w", err) + } + safeDigest := strings.ReplaceAll(layer.Digest.String(), ":", "-") + tempPath := filepath.Join(tempDir, safeDigest+".blob") + if err := os.WriteFile(tempPath, data, 0600); err != nil { + return fileresolver.LayerInfo{}, fmt.Errorf("failed to write companion temp file: %w", err) + } + return fileresolver.LayerInfo{ + TempPath: tempPath, + MediaType: string(layer.MediaType), + }, nil +} + +// fetchSingleGGUFHeader fetches a single GGUF layer header and writes it to a temp file func fetchSingleGGUFHeader(ctx context.Context, client *registryClient, ref name.Reference, layer v1.Descriptor, tempDir string) (fileresolver.LayerInfo, error) { - headerData, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxHeaderBytes) + headerData, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxWeightHeaderBytes) if err != nil { return fileresolver.LayerInfo{}, fmt.Errorf("failed to fetch GGUF layer header: %w", err) } @@ -129,7 +209,27 @@ func fetchSingleGGUFHeader(ctx context.Context, client *registryClient, ref name }, nil } -// buildMetadata constructs OCIModelMetadata from a modelArtifact. +// fetchSafeTensorsLayerHeader fetches the leading bytes of a SafeTensors weight +// layer (enough to cover the JSON header) and writes them to a temp file +func fetchSafeTensorsLayerHeader(ctx context.Context, client *registryClient, ref name.Reference, layer v1.Descriptor, tempDir string) (fileresolver.LayerInfo, error) { + headerData, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxWeightHeaderBytes) + if err != nil { + return fileresolver.LayerInfo{}, fmt.Errorf("failed to fetch safetensors layer header: %w", err) + } + + safeDigest := strings.ReplaceAll(layer.Digest.String(), ":", "-") + tempPath := filepath.Join(tempDir, safeDigest+".safetensors") + if err := os.WriteFile(tempPath, headerData, 0600); err != nil { + return fileresolver.LayerInfo{}, fmt.Errorf("failed to write temp file: %w", err) + } + + return fileresolver.LayerInfo{ + TempPath: tempPath, + MediaType: string(layer.MediaType), + }, nil +} + +// buildMetadata constructs OCIModelMetadata from a modelArtifact func buildMetadata(artifact *modelArtifact) source.OCIModelMetadata { // layers layers := make([]source.LayerMetadata, len(artifact.Manifest.Layers)) @@ -224,7 +324,8 @@ func (s *ociModelSource) Describe() source.Description { } } -// FileResolver returns a file resolver for accessing header of GGUF files. +// FileResolver returns a file resolver for accessing model headers and companion +// metadata (GGUF/SafeTensors headers, the model config blob, and companion layers). func (s *ociModelSource) FileResolver(_ source.Scope) (file.Resolver, error) { return s.resolver, nil } diff --git a/syft/source/ocimodelsource/registry_client.go b/syft/source/ocimodelsource/registry_client.go index 7574fda6c..8e6c6b9c7 100644 --- a/syft/source/ocimodelsource/registry_client.go +++ b/syft/source/ocimodelsource/registry_client.go @@ -26,9 +26,29 @@ const ( // Reference: https://www.docker.com/blog/oci-artifacts-for-ai-model-packaging/ modelConfigMediaTypePrefix = "application/vnd.docker.ai.model.config." ggufLayerMediaType = "application/vnd.docker.ai.gguf.v3" + safetensorsLayerMediaType = "application/vnd.docker.ai.safetensors" - // Maximum bytes to read/return for GGUF headers - maxHeaderBytes = 8 * 1024 * 1024 // 8 MB + // Companion metadata layers packaged alongside the weight tensors. + // model.file covers README.md / config.json / tokenizer.json / generation_config.json. + modelFileMediaType = "application/vnd.docker.ai.model.file" + licenseMediaType = "application/vnd.docker.ai.license" + + // Weight format labels surfaced on modelArtifact.Format. + modelFormatGGUF = "gguf" + modelFormatSafeTensors = "safetensors" + + // maxWeightHeaderBytes is the leading slice we range-GET from a (multi-GB) + // weight layer — enough to cover the GGUF/safetensors header. Note this is + // smaller than the ai cataloger's own maxSafeTensorsHeaderSize (100 MB) parse + // ceiling: a safetensors header between the two is parseable from a directory + // scan but truncated here, so its shard would go uncounted on an OCI scan. + // Keep this comfortably above real-world header sizes. + maxWeightHeaderBytes = 8 * 1024 * 1024 // 8 MB + + // maxCompanionBytes caps a whole companion blob (README, config.json, + // license); these are small by convention. Matches the 4 MB read cap in + // classifyOCIModelFileLayer. + maxCompanionBytes = 4 * 1024 * 1024 // 4 MB ) // registryClient handles OCI registry interactions for model artifacts. @@ -110,7 +130,20 @@ type modelArtifact struct { RawManifest []byte RawConfig []byte ManifestDigest string - GGUFLayers []v1.Descriptor + + // Format identifies the weight storage format advertised by the manifest's + // layer media types. Empty means no recognized weight layers were found. + Format string + + // GGUFLayers are descriptors for layers carrying GGUF-format weights. + // We fetch the first few MB of each to read the header data + GGUFLayers []v1.Descriptor + + // SafeTensorsLayers are descriptors for layers carrying SafeTensors-format weights. + SafeTensorsLayers []v1.Descriptor + + // CompanionLayers are non-weight layers (README, config.json, license) + CompanionLayers []v1.Descriptor } func (c *registryClient) fetchModelArtifact(ctx context.Context, refStr string) (*modelArtifact, error) { @@ -151,18 +184,38 @@ func (c *registryClient) fetchModelArtifact(ctx context.Context, refStr string) } ggufLayers := extractGGUFLayers(manifest) + safetensorsLayers := extractSafeTensorsLayers(manifest) + companionLayers := extractCompanionLayers(manifest) return &modelArtifact{ - Reference: ref, - Manifest: manifest, - Config: configFile, - RawManifest: desc.Manifest, - RawConfig: rawConfig, - ManifestDigest: desc.Digest.String(), - GGUFLayers: ggufLayers, + Reference: ref, + Manifest: manifest, + Config: configFile, + RawManifest: desc.Manifest, + RawConfig: rawConfig, + ManifestDigest: desc.Digest.String(), + Format: detectModelFormat(len(ggufLayers), len(safetensorsLayers)), + GGUFLayers: ggufLayers, + SafeTensorsLayers: safetensorsLayers, + CompanionLayers: companionLayers, }, nil } +// detectModelFormat returns a single format string when either GGUF or +// SafeTensors weight layers are present. GGUF wins if both somehow appear in one +// artifact; Docker AI artifacts carry a single weight format in practice, so the +// mixed case is not expected and the safetensors side would go uncataloged. +func detectModelFormat(ggufCount, safetensorsCount int) string { + switch { + case ggufCount > 0: + return modelFormatGGUF + case safetensorsCount > 0: + return modelFormatSafeTensors + default: + return "" + } +} + // isModelArtifact checks if the manifest represents a model artifact. func isModelArtifact(manifest *v1.Manifest) bool { return strings.HasPrefix(string(manifest.Config.MediaType), modelConfigMediaTypePrefix) @@ -179,6 +232,33 @@ func extractGGUFLayers(manifest *v1.Manifest) []v1.Descriptor { return ggufLayers } +// extractSafeTensorsLayers extracts SafeTensors weight-layer descriptors from +// the manifest. +func extractSafeTensorsLayers(manifest *v1.Manifest) []v1.Descriptor { + var out []v1.Descriptor + for _, layer := range manifest.Layers { + if string(layer.MediaType) == safetensorsLayerMediaType { + out = append(out, layer) + } + } + return out +} + +// extractCompanionLayers extracts small, non-weight layers that carry +// cataloger-relevant metadata: README.md / config.json / tokenizer.json / +// generation_config.json under vnd.docker.ai.model.file, and the LICENSE under +// vnd.docker.ai.license. +func extractCompanionLayers(manifest *v1.Manifest) []v1.Descriptor { + var out []v1.Descriptor + for _, layer := range manifest.Layers { + switch string(layer.MediaType) { + case modelFileMediaType, licenseMediaType: + out = append(out, layer) + } + } + return out +} + func (c *registryClient) fetchBlobRange(ctx context.Context, ref name.Reference, digest v1.Hash, maxBytes int64) ([]byte, error) { repo := ref.Context() @@ -189,18 +269,14 @@ func (c *registryClient) fetchBlobRange(ctx context.Context, ref name.Reference, return nil, fmt.Errorf("failed to fetch layer: %w", err) } + // Compressed() returns the raw stored blob. Docker AI weight/companion layers + // are stored uncompressed (their media types carry no +gzip suffix), so this + // is the header bytes as-is. A gzip-compressed layer would parse as garbage + // downstream rather than being transparently decompressed here. reader, err := layer.Compressed() if err != nil { return nil, fmt.Errorf("failed to get layer reader: %w", err) } - // this defer is what causes the download to stop - // 1. io.ReadFull(reader, data) reads exactly 8MB into the buffer - // 2. The function returns with data[:n] - // 3. defer reader.Close() executes, closing the HTTP response body - // 4. Closing the response body closes the underlying TCP connection - // 5. The server receives TCP FIN/RST and stops sending - // note: some data is already in flight when we close so we will see > 8mb over the wire - // the full image will not download given we terminate the reader early here defer reader.Close() // Note: this is not some arbitrary number picked out of the blue. @@ -208,8 +284,10 @@ func (c *registryClient) fetchBlobRange(ctx context.Context, ref name.Reference, // https://github.com/ggml-org/ggml/blob/master/docs/gguf.md#file-structure data := make([]byte, maxBytes) n, err := io.ReadFull(reader, data) - if err != nil && err != io.ErrUnexpectedEOF { - // ErrUnexpectedEOF is okay - it means the file is smaller than maxBytes + + // ErrUnexpectedEOF means the layer is smaller than maxBytes; EOF means it is + // empty. Both mean we read everything there was, not a failure. + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) { return nil, fmt.Errorf("failed to read layer data: %w", err) } diff --git a/syft/source/ocimodelsource/registry_client_integration_test.go b/syft/source/ocimodelsource/registry_client_integration_test.go new file mode 100644 index 000000000..9a554d340 --- /dev/null +++ b/syft/source/ocimodelsource/registry_client_integration_test.go @@ -0,0 +1,112 @@ +package ocimodelsource + +import ( + "context" + "io" + stdlog "log" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/internal/fileresolver" +) + +// TestFetchAndStore_safetensors_inMemoryRegistry drives the full OCI fetch + +// on-disk staging pipeline against a real (but in-process) registry, so nothing +// touches the network. It pushes a synthetic Docker AI safetensors artifact — +// config blob + one safetensors weight layer + a model.file companion + a +// license layer — then asserts that fetchModelArtifact classifies the layers and +// that fetchAndStoreModelHeaders stages every blob under the correct media type +// with its bytes intact. This is the seam the mock-resolver merge tests assume is +// correct; the staged content here is deliberately format-agnostic (header +// parsing is covered in the ai package). +func TestFetchAndStore_safetensors_inMemoryRegistry(t *testing.T) { + ctx := context.Background() + + configMediaType := modelConfigMediaTypePrefix + "v0.2+json" + weightHeader := []byte("safetensors-header-bytes") + modelFile := []byte(`{"architectures":["LlamaForCausalLM"],"_name_or_path":"org/model"}`) + license := []byte("MIT license text") + + img := mutate.ConfigMediaType(empty.Image, types.MediaType(configMediaType)) + img = mutate.MediaType(img, types.OCIManifestSchema1) + img, err := mutate.Append(img, + layer(weightHeader, safetensorsLayerMediaType), + layer(modelFile, modelFileMediaType), + layer(license, licenseMediaType), + ) + require.NoError(t, err) + + // in-memory registry; localhost so go-containerregistry selects the http scheme + server := httptest.NewServer(registry.New(registry.Logger(stdlog.New(io.Discard, "", 0)))) + defer server.Close() + u, err := url.Parse(server.URL) + require.NoError(t, err) + refStr := "localhost:" + u.Port() + "/testmodel:latest" + + ref, err := name.ParseReference(refStr) + require.NoError(t, err) + require.NoError(t, remote.Write(ref, img, remote.WithContext(ctx))) + + client := newRegistryClient(&image.RegistryOptions{InsecureUseHTTP: true}) + + art, err := client.fetchModelArtifact(ctx, refStr) + require.NoError(t, err) + assert.Equal(t, modelFormatSafeTensors, art.Format) + assert.Len(t, art.SafeTensorsLayers, 1) + assert.Len(t, art.CompanionLayers, 2) // model.file + license + assert.Empty(t, art.GGUFLayers) + assert.NotEmpty(t, art.RawConfig) + assert.NotEmpty(t, art.ManifestDigest) + + tempDir, resolver, err := fetchAndStoreModelHeaders(ctx, client, art) + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + assert.Equal(t, refStr, resolver.ImageReference()) + + // every blob is staged under its own media type with bytes intact — this + // exercises fetchBlobRange's short-read (ErrUnexpectedEOF) branch, since each + // blob is far smaller than the fetch cap, plus all four staging helpers. + assertServesBytes(t, resolver, safetensorsLayerMediaType, weightHeader) + assertServesBytes(t, resolver, modelFileMediaType, modelFile) + assertServesBytes(t, resolver, licenseMediaType, license) + + cfgLocs, err := resolver.FilesByMediaType(configMediaType) + require.NoError(t, err) + require.Len(t, cfgLocs, 1) +} + +func layer(content []byte, mediaType string) mutate.Addendum { + return mutate.Addendum{ + Layer: static.NewLayer(content, types.MediaType(mediaType)), + MediaType: types.MediaType(mediaType), + } +} + +func assertServesBytes(t *testing.T, resolver *fileresolver.ContainerImageModel, mediaType string, want []byte) { + t.Helper() + locs, err := resolver.FilesByMediaType(mediaType) + require.NoError(t, err) + require.Len(t, locs, 1) + + rc, err := resolver.FileContentsByLocation(locs[0]) + require.NoError(t, err) + defer rc.Close() + + got, err := io.ReadAll(rc) + require.NoError(t, err) + assert.Equal(t, want, got) +} diff --git a/syft/source/ocimodelsource/registry_client_safetensors_test.go b/syft/source/ocimodelsource/registry_client_safetensors_test.go new file mode 100644 index 000000000..2ca91b4a2 --- /dev/null +++ b/syft/source/ocimodelsource/registry_client_safetensors_test.go @@ -0,0 +1,55 @@ +package ocimodelsource + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/source" +) + +func TestDetectModelFormat(t *testing.T) { + tests := []struct { + name string + gguf int + safetensors int + expected string + }{ + {name: "gguf only", gguf: 2, safetensors: 0, expected: modelFormatGGUF}, + {name: "safetensors only", gguf: 0, safetensors: 3, expected: modelFormatSafeTensors}, + {name: "both prefers gguf", gguf: 1, safetensors: 1, expected: modelFormatGGUF}, + {name: "neither", gguf: 0, safetensors: 0, expected: ""}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, detectModelFormat(test.gguf, test.safetensors)) + }) + } +} + +func TestExtractSafeTensorsLayers(t *testing.T) { + manifest := &v1.Manifest{Layers: []v1.Descriptor{ + {MediaType: types.MediaType(safetensorsLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "a"}}, + {MediaType: types.MediaType(ggufLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "b"}}, + {MediaType: types.MediaType(safetensorsLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "c"}}, + }} + assert.Len(t, extractSafeTensorsLayers(manifest), 2) +} + +func TestExtractCompanionLayers(t *testing.T) { + manifest := &v1.Manifest{Layers: []v1.Descriptor{ + {MediaType: types.MediaType(modelFileMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "readme"}}, + {MediaType: types.MediaType(licenseMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "license"}}, + {MediaType: types.MediaType(safetensorsLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "weights"}}, + {MediaType: types.DockerLayer, Digest: v1.Hash{Algorithm: "sha256", Hex: "other"}}, + }} + // only the model.file and license layers should be selected (not weights or arbitrary layers) + assert.Len(t, extractCompanionLayers(manifest), 2) +} + +func TestCalculateTotalSize(t *testing.T) { + layers := []source.LayerMetadata{{Size: 100}, {Size: 250}, {Size: 0}} + assert.Equal(t, int64(350), calculateTotalSize(layers)) +} From 8ed534c32bc4b43fdc0111743c26ff9a328c53b5 Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Wed, 15 Jul 2026 20:55:42 -0400 Subject: [PATCH 074/168] fix: NPM redis.js CPE (#5012) Signed-off-by: Rez Moss --- .../internal/cpegenerate/candidate_by_package_type.go | 5 +++++ .../cpegenerate/candidate_by_package_type_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go index 32c2131a1..b43906c8b 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go +++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go @@ -221,6 +221,11 @@ var defaultCandidateAdditions = buildCandidateLookup( candidateKey{PkgName: "mustache"}, candidateAddition{AdditionalProducts: []string{"mustache.js"}}, }, + { + pkg.NpmPkg, + candidateKey{PkgName: "redis"}, + candidateAddition{AdditionalVendors: []string{"redis.js"}}, + }, // Gem packages { diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go index 4d903b14c..8f21eeae4 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go @@ -147,6 +147,14 @@ func Test_additionalVendors(t *testing.T) { vendor: "my-vendor", expected: []string{"awesome-vendor-addition", "one-good-addition", "another-good-addition"}, }, + { + name: "default additionsnpm redis gets redisjs vendor", + allAdditions: defaultCandidateAdditions, + ty: pkg.NpmPkg, + pkgName: "redis", + vendor: "redis", + expected: []string{"redis.js"}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { From 5e11775c8b76bd6cedc53e3c60cc02544575f0d5 Mon Sep 17 00:00:00 2001 From: Yashas Gunderia <68075205+ychampion@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:39:58 +0530 Subject: [PATCH 075/168] feat: detect unpackaged Firefox in software inventories (#5068) Signed-off-by: ychampion --- syft/pkg/cataloger/binary/capabilities.yaml | 10 ++++++ .../binary/classifier_cataloger_test.go | 11 +++++++ syft/pkg/cataloger/binary/classifiers.go | 13 ++++++++ .../negative/firefox/application.ini | 3 ++ .../classifiers/negative/firefox/firefox | 1 + .../firefox/151.0/linux-amd64/application.ini | 29 ++++++++++++++++++ .../firefox/151.0/linux-amd64/firefox | Bin 0 -> 325 bytes .../pkg/cataloger/binary/testdata/config.yaml | 9 ++++++ 8 files changed, 76 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/firefox diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index 18e265ae0..17f36be0b 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -666,6 +666,16 @@ catalogers: cpes: - cpe:2.3:a:google:chrome:*:*:*:*:*:*:*:* type: BinaryPkg + - method: glob + criteria: + - '**/{firefox,firefox.exe}' + packages: + - class: firefox-binary + name: firefox + purl: pkg:generic/firefox + cpes: + - cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:* + type: BinaryPkg - method: glob criteria: - '**/ffmpeg' diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index ab5af2703..286699a76 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -1951,6 +1951,17 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("chrome-binary"), }, }, + { + logicalFixture: "firefox/151.0/linux-amd64", + expected: pkg.Package{ + Name: "firefox", + Version: "151.0", + Type: "binary", + PURL: "pkg:generic/firefox@151.0", + Locations: locations("firefox", "application.ini"), + Metadata: metadata("firefox-binary"), + }, + }, { logicalFixture: "ffmpeg/7.1.1/darwin-arm64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 52a9be871..9c5adeea4 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -913,6 +913,19 @@ func DefaultClassifiers() []binutils.Classifier { PURL: mustPURL("pkg:generic/chrome@version"), CPEs: singleCPE("cpe:2.3:a:google:chrome:*:*:*:*:*:*:*:*"), }, + { + Class: "firefox-binary", + FileGlob: "**/{firefox,firefox.exe}", + EvidenceMatcher: binutils.SupportingEvidenceMatcher( + "application.ini", + m.FileContentsVersionMatcher( + `(?ms)^Name=Firefox\r?$.*?^Version=(?P[0-9]+(?:\.[0-9]+)+(?:[A-Za-z][0-9A-Za-z.-]*)?)\r?$`, + ), + ), + Package: "firefox", + PURL: mustPURL("pkg:generic/firefox@version"), + CPEs: singleCPE("cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:*"), + }, { Class: "ffmpeg-binary", FileGlob: "**/ffmpeg", diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini new file mode 100644 index 000000000..1a917ca96 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini @@ -0,0 +1,3 @@ +[App] +Name=Thunderbird +Version=151.0 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox new file mode 100644 index 000000000..b1dc0970c --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox @@ -0,0 +1 @@ +not a Firefox executable diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini new file mode 100644 index 000000000..eca8b2888 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini @@ -0,0 +1,29 @@ +name: application.ini +offset: 0 +length: 641 +snippetSha256: 286d4471ecf20596e8abde1ec5cb11456db06a6f73593c7b983f502b0b9b3011 +fileSha256: 286d4471ecf20596e8abde1ec5cb11456db06a6f73593c7b983f502b0b9b3011 + +### byte snippet to follow ### +; This file is not used. If you modify it and want the application to use +; your modifications, move it under the browser/ subdirectory and start with +; the "-app /path/to/browser/application.ini" argument. +[App] +Vendor=Mozilla +Name=Firefox +RemotingName=firefox-default +CodeName=Nightly +Version=151.0 +BuildID=20260611122700 +ID={ec8030f7-c20a-464f-9b0e-13a3a9e97384} + +[Gecko] +MinVersion=151.0 +MaxVersion=151.0 + +[XRE] +EnableProfileMigrator=1 + + +[AppUpdate] +URL=https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/firefox b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/firefox new file mode 100644 index 0000000000000000000000000000000000000000..09fd7009b13619b127e891ac959446c0a64ec46e GIT binary patch literal 325 zcmZ9GOG?B*5Qck35otVwHsk{RsD301$wpW1+zb7vnhC^Ya02Set9T;MWHV%3SWs2` z|5yCf6ko9uG`u0rAABY{;?fDpN1S%cz7tAHew>EG0hi}})Yf&PMenRbWm55~P|5@f z$U_dAN);Gm63!2Aqeis$-uZO3{?PkNyb;0i0Nxf`O gR=)!AbA3KxJqzrn&O3I?uBv-kyI<-5{as^z0ZSc5od5s; literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml index 5f8fa88aa..3bab3a99c 100644 --- a/syft/pkg/cataloger/binary/testdata/config.yaml +++ b/syft/pkg/cataloger/binary/testdata/config.yaml @@ -17,6 +17,15 @@ from-images: paths: - /bin/[ + - name: firefox + version: "151.0" + images: + - ref: mcr.microsoft.com/playwright:v1.61.1-noble@sha256:cf0daee9b994042e011bc29f20cdff1a9f682a039b43fcd738f7d8a9d3bcd9d6 + platform: linux/amd64 + paths: + - /ms-playwright/firefox-1532/firefox/firefox + - /ms-playwright/firefox-1532/firefox/application.ini + - name: util-linux version: 2.37.4 images: From 3e2bc6ed095f7ec1a415fb38cfe1c319e95dfed6 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:21:22 -0400 Subject: [PATCH 076/168] ci: add docker-container driver (#5071) * ci: add docker-container driver Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: update to v4.2.0 Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .github/workflows/release.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 39e4fe7da..35f09eeab 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,6 +83,11 @@ jobs: - name: Bootstrap environment uses: ./.github/actions/bootstrap + # dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a + # docker-container driver builder (the default builder on the runner cannot build multi-platform). + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - name: Login to Docker Hub uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0 with: From 36a3af41d95e179c79901a3e205b41cacd55b72c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:32:07 +0000 Subject: [PATCH 077/168] chore(deps): bump golang.org/x/net from 0.56.0 to 0.57.0 (#5076) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0. - [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.57.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index dc3109696..b220b7d54 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/mod v0.37.0 - golang.org/x/net v0.56.0 + golang.org/x/net v0.57.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.47.0 gopkg.in/yaml.v3 v3.0.1 @@ -309,12 +309,12 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.53.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/api v0.271.0 // indirect diff --git a/go.sum b/go.sum index bb20bd808..a18873431 100644 --- a/go.sum +++ b/go.sum @@ -1040,8 +1040,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1127,8 +1127,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1160,8 +1160,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1236,13 +1236,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1253,8 +1253,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From dc1ef98237e6df35d4cbe3ea410bbeef33769788 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:52:22 +0000 Subject: [PATCH 078/168] chore(deps): bump golang.org/x/tools from 0.47.0 to 0.48.0 (#5077) Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.47.0 to 0.48.0. - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.47.0...v0.48.0) --- updated-dependencies: - dependency-name: golang.org/x/tools dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b220b7d54..ec554138d 100644 --- a/go.mod +++ b/go.mod @@ -93,10 +93,10 @@ require ( go.uber.org/goleak v1.3.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f - golang.org/x/mod v0.37.0 + golang.org/x/mod v0.38.0 golang.org/x/net v0.57.0 golang.org/x/time v0.15.0 - golang.org/x/tools v0.47.0 + golang.org/x/tools v0.48.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.53.0 ) diff --git a/go.sum b/go.sum index a18873431..f677e0766 100644 --- a/go.sum +++ b/go.sum @@ -1081,8 +1081,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1315,8 +1315,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From ae9534203d4a49f2b96a05dfc1383e2376993c72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:52:25 +0000 Subject: [PATCH 079/168] chore(deps): bump golang.org/x/mod from 0.37.0 to 0.38.0 (#5078) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.37.0 to 0.38.0. - [Commits](https://github.com/golang/mod/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/mod dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From d380ad08b5945058ca4c4662ca1f3a8dfd7db7f9 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 20 Jul 2026 11:21:24 -0400 Subject: [PATCH 080/168] chore(deps): update CPE dictionary index (#5082) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 694bf48a5..b9385c447 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -7,6 +7,9 @@ "chainguard.dev/apko": [ "cpe:2.3:a:chainguard:apko:*:*:*:*:*:go:*:*" ], + "github.com/Azure/go-ntlmssp": [ + "cpe:2.3:a:microsoft:go-ntlmssp:*:*:*:*:*:go:*:*" + ], "github.com/ClickHouse/ch-go": [ "cpe:2.3:a:clickhouse:ch:*:*:*:*:*:go:*:*" ], @@ -223,6 +226,9 @@ "github.com/xddxdd/bird-lg-go": [ "cpe:2.3:a:xddxdd:bird-lg-go:*:*:*:*:*:go:*:*" ], + "github.com/xuri/excelize/v2": [ + "cpe:2.3:a:excelize:excelize:*:*:*:*:*:go:*:*" + ], "github.com/yuin/goldmark": [ "cpe:2.3:a:yuin:goldmark:*:*:*:*:*:go:*:*" ], @@ -2349,6 +2355,9 @@ "@apollosproject/data-connector-rock": [ "cpe:2.3:a:apollosapp:data-connector-rock:*:*:*:*:*:node.js:*:*" ], + "@appium/base-driver": [ + "cpe:2.3:a:appium:appium\\/base-driver:*:*:*:*:*:node.js:*:*" + ], "@appium/support": [ "cpe:2.3:a:appium:appium\\/support:*:*:*:*:*:node.js:*:*" ], @@ -2616,6 +2625,9 @@ "@octokit/webhooks": [ "cpe:2.3:a:octokit:webhooks:*:*:*:*:*:node.js:*:*" ], + "@openclaw/feishu": [ + "cpe:2.3:a:openclaw:openclaw\\/feishu:*:*:*:*:*:node.js:*:*" + ], "@openclaw/voice-call": [ "cpe:2.3:a:openclaw:openclaw\\/voice-call:*:*:*:*:*:node.js:*:*" ], @@ -3086,6 +3098,9 @@ "cli": [ "cpe:2.3:a:cli_project:cli:*:*:*:*:*:node.js:*:*" ], + "client-certificate-auth": [ + "cpe:2.3:a:tgies:client-certificate-auth:*:*:*:*:*:node.js:*:*" + ], "closure-compiler-stream": [ "cpe:2.3:a:closure-compiler-stream_project:closure-compiler-stream:*:*:*:*:*:node.js:*:*" ], @@ -5752,6 +5767,9 @@ "ssrf-agent": [ "cpe:2.3:a:ssrf-agent_project:ssrf-agent:*:*:*:*:*:node.js:*:*" ], + "ssrfcheck": [ + "cpe:2.3:a:felippe-regazio:ssrf_check:*:*:*:*:*:node.js:*:*" + ], "ssri": [ "cpe:2.3:a:ssri_project:ssri:*:*:*:*:*:node.js:*:*" ], @@ -6978,6 +6996,9 @@ "sqlparse": [ "cpe:2.3:a:sqlparse_project:sqlparse:*:*:*:*:*:python:*:*" ], + "stanza": [ + "cpe:2.3:a:stanford:stanza:*:*:*:*:*:python:*:*" + ], "starlette": [ "cpe:2.3:a:encode:starlette:*:*:*:*:*:python:*:*" ], @@ -7690,6 +7711,12 @@ "bzip2": [ "cpe:2.3:a:bzip2_project:bzip2:*:*:*:*:*:rust:*:*" ], + "c2pa": [ + "cpe:2.3:a:adobe:c2pa:*:*:*:*:*:rust:*:*" + ], + "c2patool": [ + "cpe:2.3:a:adobe:c2patool:*:*:*:*:*:*:*:*" + ], "cache": [ "cpe:2.3:a:cache_project:cache:*:*:*:*:*:rust:*:*" ], @@ -9175,7 +9202,8 @@ "cpe:2.3:a:article_directory_project:article_directory:*:*:*:*:*:wordpress:*:*" ], "article-directory-redux": [ - "cpe:2.3:a:icontrolwp:article_directory_redux:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:icontrolwp:article_directory_redux:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:icontrolwp:icontrolwp:*:*:*:*:*:wordpress:*:*" ], "article2pdf": [ "cpe:2.3:a:article2pdf_project:article2pdf:*:*:*:*:*:wordpress:*:*" @@ -13831,6 +13859,9 @@ "ibtana-visual-editor": [ "cpe:2.3:a:vowelweb:ibtana:*:*:*:*:*:wordpress:*:*" ], + "ibuildapp": [ + "cpe:2.3:a:ibuildapp:ibuildapp:*:*:*:*:*:wordpress:*:*" + ], "icegram": [ "cpe:2.3:a:icegram:icegram_engage:*:*:*:*:*:wordpress:*:*" ], @@ -15508,7 +15539,7 @@ "cpe:2.3:a:motor_racing_league_project:motor_racing_league:*:*:*:*:*:wordpress:*:*" ], "motors-car-dealership-classified-listings": [ - "cpe:2.3:a:stylemixthemes:motors_-_car_dealer\\,_classifieds_\\\u0026_listing:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:stylemixthemes:motors_-_car_dealer\\,_classifieds_\\\u0026_listing:*:*:*:*:starter:wordpress:*:*" ], "mousewheel-smooth-scroll": [ "cpe:2.3:a:mousewheel_smooth_scroll_project:mousewheel_smooth_scroll:*:*:*:*:*:wordpress:*:*" From a0852ebf987efdd44a5fa33e26cc12dbcff0ae1f Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 20 Jul 2026 11:28:29 -0400 Subject: [PATCH 081/168] Add multi-platform OCI image support (#5074) * Add multi-platform OCI image support Signed-off-by: Jason Paulos * Reduce calls to PrepareMultiplatformFixtureImage in TestMultiPlatformOCIImageSelection Signed-off-by: Jason Paulos * Use smaller image for testing & update stereoscope fork Signed-off-by: Jason Paulos * bump to stereoscope@main after 548 merge Signed-off-by: Alex Goodman --------- Signed-off-by: Jason Paulos Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- .../integration/multiplatform_oci_test.go | 145 ++++++++++++++++++ .../test/integration/testdata/.gitignore | 4 +- go.mod | 14 +- go.sum | 28 ++-- 4 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 cmd/syft/internal/test/integration/multiplatform_oci_test.go diff --git a/cmd/syft/internal/test/integration/multiplatform_oci_test.go b/cmd/syft/internal/test/integration/multiplatform_oci_test.go new file mode 100644 index 000000000..94879a190 --- /dev/null +++ b/cmd/syft/internal/test/integration/multiplatform_oci_test.go @@ -0,0 +1,145 @@ +package integration + +import ( + "context" + "encoding/json" + "fmt" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/stereoscope/pkg/imagetest" + "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/source" +) + +// TestMultiPlatformOCIImageSelection verifies that syft can load a single platform out of a +// multi-platform OCI image on disk (both --from oci-dir and --from oci-archive) by selecting the +// image that matches the requested platform. +func TestMultiPlatformOCIImageSelection(t *testing.T) { + remoteImage := "docker.io/library/busybox:1.38.0" + + // Per-platform image config digests within the multi-platform index, obtained from the OCI layout + // (see anchore/stereoscope integration tests: TestPlatformSelectionWithOciLocalSources). + expectedDigest := map[string]string{ + "arm64": "sha256:e0e8b3cbfed68a90084781e2962f9c0deead51c5a3f11a488eef0283a4284bc2", + "s390x": "sha256:0cf160e720a8e4f20883b72276a6eaded83b79539f3f1d39e35a3336154b9960", + "amd64": "sha256:c6348fa86ba0fb2108c9334f5fe913ddc6d853313e655891f133a0127c30099f", + "ppc64le": "sha256:b144fc0e06537d07956cde340b878a81a717e394edb736d3110858a12a6635cb", + } + + // syft --from source tag -> stereoscope OCI source used to prepare the local fixture + sources := map[string]image.Source{ + "oci-dir": image.OciDirectorySource, + "oci-archive": image.OciTarballSource, + } + + for from, imageSource := range sources { + t.Run(from, func(t *testing.T) { + localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage) + for _, arch := range []string{"amd64", "arm64", "s390x", "ppc64le"} { + t.Run(fmt.Sprintf("linux/%s", arch), func(t *testing.T) { + platform, err := image.NewPlatform("linux/" + arch) + require.NoError(t, err) + + src, err := syft.GetSource( + context.Background(), + localPath, + syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, src.Close()) + }) + + meta, ok := src.Describe().Metadata.(source.ImageMetadata) + require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata) + + // The raw config of the selected image must match the requested platform... + assertConfigPlatform(t, meta.RawConfig, "linux", arch) + // ...and it must be the exact per-platform image from the multi-platform index. + assert.Equal(t, expectedDigest[arch], meta.ID) + }) + } + }) + } +} + +// TestMultiPlatformOCIImageSelection_UnavailablePlatform verifies that requesting a platform not present +// in the multi-platform OCI image results in an error rather than silently selecting the wrong image. +func TestMultiPlatformOCIImageSelection_UnavailablePlatform(t *testing.T) { + remoteImage := "docker.io/library/busybox:1.38.0" + + // windows/amd64 is not present in this linux-only multi-platform image + platform, err := image.NewPlatform("windows/amd64") + require.NoError(t, err) + + sources := map[string]image.Source{ + "oci-dir": image.OciDirectorySource, + "oci-archive": image.OciTarballSource, + } + + for from, imageSource := range sources { + t.Run(from, func(t *testing.T) { + localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage) + + _, err := syft.GetSource( + context.Background(), + localPath, + syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform), + ) + require.ErrorContains(t, err, "windows/amd64") + }) + } +} + +// TestMultiPlatformOCIImageSelection_DefaultPlatform verifies that not specifying a platform results +// in the current platform being selected. +func TestMultiPlatformOCIImageSelection_DefaultPlatform(t *testing.T) { + remoteImage := "docker.io/library/busybox:1.38.0" + + sources := map[string]image.Source{ + "oci-dir": image.OciDirectorySource, + "oci-archive": image.OciTarballSource, + } + + for from, imageSource := range sources { + t.Run(from, func(t *testing.T) { + localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage) + + src, err := syft.GetSource( + context.Background(), + localPath, + syft.DefaultGetSourceConfig().WithSources(from), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, src.Close()) + }) + + meta, ok := src.Describe().Metadata.(source.ImageMetadata) + require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata) + + // The raw config of the selected image must match the current platform + assertConfigPlatform(t, meta.RawConfig, "linux", runtime.GOARCH) + }) + } +} + +// assertConfigPlatform asserts the os/architecture recorded in a raw OCI image config document. +func assertConfigPlatform(t *testing.T, rawConfig []byte, os, architecture string) { + t.Helper() + require.NotEmpty(t, rawConfig) + + var cfg struct { + OS string `json:"os"` + Architecture string `json:"architecture"` + } + require.NoError(t, json.Unmarshal(rawConfig, &cfg)) + + assert.Equal(t, os, cfg.OS) + assert.Equal(t, architecture, cfg.Architecture) +} diff --git a/cmd/syft/internal/test/integration/testdata/.gitignore b/cmd/syft/internal/test/integration/testdata/.gitignore index 48c7e0961..ddc9f73c0 100644 --- a/cmd/syft/internal/test/integration/testdata/.gitignore +++ b/cmd/syft/internal/test/integration/testdata/.gitignore @@ -7,4 +7,6 @@ !image-pkg-coverage/pkgs/java/*.hpi **/go.sum -!image-go-bin-arch-coverage/go.sum \ No newline at end of file +!image-go-bin-arch-coverage/go.sum + +cache/ \ No newline at end of file diff --git a/go.mod b/go.mod index ec554138d..0a29a383e 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/anchore/go-sync v0.1.1 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.2.0 - github.com/anchore/stereoscope v0.2.2 + github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef @@ -40,7 +40,7 @@ require ( github.com/elliotchance/phpserialize v1.4.0 github.com/facebookincubator/nvdtools v0.1.5 github.com/github/go-spdx/v2 v2.7.0 - github.com/gkampitakis/go-snaps v0.5.22 + github.com/gkampitakis/go-snaps v0.5.23 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.1 github.com/go-test/deep v1.1.1 @@ -171,7 +171,7 @@ require ( github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect github.com/containerd/containerd/api v1.11.1 // indirect - github.com/containerd/containerd/v2 v2.3.2 // indirect + github.com/containerd/containerd/v2 v2.3.3 // indirect github.com/containerd/continuity v0.5.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -183,7 +183,7 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v29.5.3+incompatible // indirect + github.com/docker/cli v29.6.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -238,8 +238,8 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/moby/moby/api v1.54.2 // indirect - github.com/moby/moby/client v0.4.1 // indirect + github.com/moby/moby/api v1.55.0 // indirect + github.com/moby/moby/client v0.5.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/user v0.4.0 // indirect @@ -257,7 +257,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/pborman/indent v1.2.1 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/piprate/json-gold v0.7.0 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect diff --git a/go.sum b/go.sum index f677e0766..1c8d9df09 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= -github.com/anchore/stereoscope v0.2.2 h1:SGTLGoF6GHmKEn9Bb6LYpzzgz9nhvMZgN/c4fTSQjYY= -github.com/anchore/stereoscope v0.2.2/go.mod h1:ylJXJKebLctP7u9ewguB1d0zUETJxvAA7r2DiuljDTM= +github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567 h1:ZklP3dHyXFUKPrZaQcqP6M86KzlbpayiEvUUeQRT/BI= +github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -296,8 +296,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM= -github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= +github.com/containerd/containerd/v2 v2.3.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k= +github.com/containerd/containerd/v2 v2.3.3/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -339,8 +339,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= -github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8= +github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -410,8 +410,8 @@ github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XF github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z0KY= github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-snaps v0.5.22 h1:xg9omphRnbDnimMCl1KqznC4krlxOGpkB0vDSfX2P7M= -github.com/gkampitakis/go-snaps v0.5.22/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM= +github.com/gkampitakis/go-snaps v0.5.23 h1:okh5QR48zpUjpWtu65AtqxdCY8huJq+dEDuUzd1PuKg= +github.com/gkampitakis/go-snaps v0.5.23/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM= github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -723,10 +723,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= -github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= -github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= @@ -786,8 +786,8 @@ github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrp github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/piprate/json-gold v0.7.0 h1:bEMirgA5y8Z2loTQfxyIFfY+EflxH1CTP6r/KIlcJNw= From 22ffd5209dbc5a4a2f0f4d732997f12c0830aac0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:28:53 +0000 Subject: [PATCH 082/168] chore(deps): bump github.com/gkampitakis/go-snaps from 0.5.22 to 0.5.23 (#5079) Bumps [github.com/gkampitakis/go-snaps](https://github.com/gkampitakis/go-snaps) from 0.5.22 to 0.5.23. - [Release notes](https://github.com/gkampitakis/go-snaps/releases) - [Commits](https://github.com/gkampitakis/go-snaps/compare/v0.5.22...v0.5.23) --- updated-dependencies: - dependency-name: github.com/gkampitakis/go-snaps dependency-version: 0.5.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 289137ff3c101470597e1f53e566ed08b75ee651 Mon Sep 17 00:00:00 2001 From: Yashas Gunderia <68075205+ychampion@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:00:56 +0530 Subject: [PATCH 083/168] Avoid duplicate packages for replaced Go modules (#5069) * Prevent duplicate Go packages after source replacement resolution Source analysis already applies module replacements through go/packages. Avoid synthesizing the same replacement again from go.mod while retaining fallback synthesis for modules that source analysis did not resolve. Constraint: Preserve unimported and local-path replacement cataloging. Rejected: Deduplicate only during final assembly | That retains redundant license lookup and ambiguous metadata ownership. Confidence: high Scope-risk: narrow Directive: Keep go.mod fallback packages limited to modules absent from source analysis. Tested: Focused replacement regression, related Go module parser tests, go vet, gofmt, and diff checks. Not-tested: Docker-backed full cataloger fixtures; local root storage was exhausted by image generation. Signed-off-by: ychampion * Keep replacement fixtures with the Go module test data Constraint: The maintainer reserves internal/gotestdata for fixtures that need special Go tooling discovery. Rejected: Leave this fixture in gotestdata | The regression opens its module explicitly and does not need the special location. Confidence: high Scope-risk: narrow Directive: Use internal/gotestdata only when a fixture must avoid Go testdata discovery rules. Tested: replacement regression repeated 10 times; Go module parser table; go vet for the Go cataloger; gofmt; diff checks. Not-tested: Full cataloger package; three unrelated parser fixtures fail identically on exact prior head in this environment. Signed-off-by: ychampion --------- Signed-off-by: ychampion Co-authored-by: ychampion --- syft/pkg/cataloger/golang/parse_go_mod.go | 7 ++- .../golang/parse_go_mod_replace_test.go | 44 +++++++++++++++++++ .../testdata/go-source-replacements/go.mod | 15 +++++++ .../testdata/go-source-replacements/go.sum | 6 +++ .../testdata/go-source-replacements/main.go | 11 +++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 syft/pkg/cataloger/golang/parse_go_mod_replace_test.go create mode 100644 syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod create mode 100644 syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum create mode 100644 syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go diff --git a/syft/pkg/cataloger/golang/parse_go_mod.go b/syft/pkg/cataloger/golang/parse_go_mod.go index d593ed299..4a40403b1 100644 --- a/syft/pkg/cataloger/golang/parse_go_mod.go +++ b/syft/pkg/cataloger/golang/parse_go_mod.go @@ -72,7 +72,7 @@ func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resol // only use go.mod packages NOT found in source analysis goModPackages := c.createGoModPackages(ctx, resolver, modFile, sourceModules, reader, digests) - c.applyReplaceDirectives(ctx, resolver, modFile, goModPackages, reader, digests) + c.applyReplaceDirectives(ctx, resolver, modFile, sourceModules, goModPackages, reader, digests) c.applyExcludeDirectives(modFile, goModPackages) pkgs = c.assembleResults(catalogedModules, goModPackages) @@ -365,8 +365,11 @@ func (c *goModCataloger) createGoModPackages(ctx context.Context, resolver file. } // applyReplaceDirectives processes replace directives from go.mod -func (c *goModCataloger) applyReplaceDirectives(ctx context.Context, resolver file.Resolver, modFile *modfile.File, goModPackages map[string]pkg.Package, reader file.LocationReadCloser, digests map[string]string) { +func (c *goModCataloger) applyReplaceDirectives(ctx context.Context, resolver file.Resolver, modFile *modfile.File, sourceModules map[string]*packages.Module, goModPackages map[string]pkg.Package, reader file.LocationReadCloser, digests map[string]string) { for _, m := range modFile.Replace { + if sourceModules != nil && sourceModules[m.Old.Path] != nil { + continue + } lics := c.licenseResolver.getLicenses(ctx, resolver, m.New.Path, m.New.Version) var finalPath string if !strings.HasPrefix(m.New.Path, ".") && !strings.HasPrefix(m.New.Path, "/") { diff --git a/syft/pkg/cataloger/golang/parse_go_mod_replace_test.go b/syft/pkg/cataloger/golang/parse_go_mod_replace_test.go new file mode 100644 index 000000000..8e51fd2a8 --- /dev/null +++ b/syft/pkg/cataloger/golang/parse_go_mod_replace_test.go @@ -0,0 +1,44 @@ +package golang + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + stereofile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" + "github.com/anchore/syft/syft/source/directorysource" +) + +func Test_parseGoSource_replacedModulesAreNotDuplicated(t *testing.T) { + fixture := filepath.Join("testdata", "go-source-replacements") + s, err := directorysource.NewFromPath(fixture) + require.NoError(t, err) + resolver, err := s.FileResolver(source.AllLayersScope) + require.NoError(t, err) + + modPath, err := filepath.Abs(filepath.Join(fixture, "go.mod")) + require.NoError(t, err) + contents, err := os.Open(modPath) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, contents.Close()) }) + + reader := file.LocationReadCloser{ + Location: file.NewVirtualLocationFromDirectory("go.mod", "go.mod", *stereofile.NewFileReference(stereofile.Path(modPath))), + ReadCloser: contents, + } + config := DefaultCatalogerConfig().WithUsePackagesLib(true).WithSearchRemoteLicenses(false) + pkgs, _, err := newGoModCataloger(config).parseGoModFile(pkgtest.Context(t), resolver, nil, reader) + require.NoError(t, err) + + versions := make(map[string][]string) + for _, p := range pkgs { + versions[p.Name] = append(versions[p.Name], p.Version) + } + require.Equal(t, []string{"v1.5.2"}, versions["rsc.io/quote"]) + require.Equal(t, []string{"v1.3.1"}, versions["rsc.io/sampler"]) +} diff --git a/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod new file mode 100644 index 000000000..b9066bb7c --- /dev/null +++ b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod @@ -0,0 +1,15 @@ +module example.com/repro + +go 1.23 + +require rsc.io/quote v1.5.1 + +require ( + golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect + rsc.io/sampler v1.3.0 // indirect +) + +replace ( + rsc.io/quote => rsc.io/quote v1.5.2 + rsc.io/sampler => rsc.io/sampler v1.3.1 +) diff --git a/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum new file mode 100644 index 000000000..715747812 --- /dev/null +++ b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum @@ -0,0 +1,6 @@ +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= +rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= +rsc.io/sampler v1.3.1 h1:F0c3J2nQCdk9ODsNhU3sElnvPIxM/xV1c/qZuAeZmac= +rsc.io/sampler v1.3.1/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go b/syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go new file mode 100644 index 000000000..0a29866c4 --- /dev/null +++ b/syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + + "rsc.io/quote" +) + +func main() { + fmt.Println(quote.Hello()) +} From 29fd7d0dec81cf03e0a1194a1985c7c893bb2396 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 20 Jul 2026 14:04:57 -0400 Subject: [PATCH 084/168] chore(deps): update anchore dependencies (#5022) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 0a29a383e..63d4e8378 100644 --- a/go.mod +++ b/go.mod @@ -18,11 +18,11 @@ require ( github.com/anchore/go-homedir v0.1.1 github.com/anchore/go-logger v0.1.1 github.com/anchore/go-macholibre v0.1.1 - github.com/anchore/go-rpmdb v0.1.0 + github.com/anchore/go-rpmdb v0.2.0 github.com/anchore/go-sync v0.1.1 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.2.0 - github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567 + github.com/anchore/stereoscope v0.3.0 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef diff --git a/go.sum b/go.sum index 1c8d9df09..12fe1ee88 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM= github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M= github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro= -github.com/anchore/go-rpmdb v0.1.0 h1:Q8dc208/HYzCqhx0L1zurfm1UPil24hlo9NjkdFmLdE= -github.com/anchore/go-rpmdb v0.1.0/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU= +github.com/anchore/go-rpmdb v0.2.0 h1:ixcyMg/guhBL9XjBVwhTv9CG48xO07Qkg2uDX2+zx/c= +github.com/anchore/go-rpmdb v0.2.0/go.mod h1:ATsRlpCXstnoYfzqBfhwGw0U2dolx1BBQ9rAU8jWBAw= github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4= github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw= github.com/anchore/go-sync v0.1.1 h1:91SZ+YqUIIHmf2jPAYZPuHqM/ZqnK1pVJDWtET6+AJE= @@ -146,8 +146,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= -github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567 h1:ZklP3dHyXFUKPrZaQcqP6M86KzlbpayiEvUUeQRT/BI= -github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI= +github.com/anchore/stereoscope v0.3.0 h1:6mGGRxm4zRKj/jNiSM7g6XGwWihrnkQRJj11hcULDqY= +github.com/anchore/stereoscope v0.3.0/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= From c890e7f17f8db638d3f1558bf087b4cb2f3c2aad Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 23 Jul 2026 15:03:36 -0400 Subject: [PATCH 085/168] decode golang symbols (#5089) Signed-off-by: Alex Goodman --- syft/pkg/cataloger/golang/symbols.go | 56 +++++++++++++++++++++-- syft/pkg/cataloger/golang/symbols_test.go | 52 +++++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go index 1d8067dff..3b0987f2e 100644 --- a/syft/pkg/cataloger/golang/symbols.go +++ b/syft/pkg/cataloger/golang/symbols.go @@ -52,10 +52,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) { continue } seen[fn.Name] = struct{}{} - syms = append(syms, binarySymbol{ - packagePath: fn.PackageName(), - name: fn.Name, - }) + syms = append(syms, makeBinarySymbol(fn.Name, fn.PackageName())) } // debug/gosym only exposes top-level functions; functions that the compiler inlined into their @@ -71,7 +68,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) { continue } seen[name] = struct{}{} - syms = append(syms, binarySymbol{packagePath: pkgPath, name: name}) + syms = append(syms, makeBinarySymbol(name, pkgPath)) } return syms, nil @@ -94,6 +91,55 @@ func packagePathFromSymbolName(name string) string { return name[:slash+1+dot] } +// makeBinarySymbol builds a binarySymbol, unescaping the %xx sequences the go linker introduces in the +// import-path portion of a symbol name (see unescapePackagePath). The local-symbol suffix (method and +// function names) is left untouched, so only the path prefix shared by packagePath and name is rewritten. +func makeBinarySymbol(name, pkgPath string) binarySymbol { + unescaped := unescapePackagePath(pkgPath) + if unescaped != pkgPath && strings.HasPrefix(name, pkgPath) { + name = unescaped + name[len(pkgPath):] + } + return binarySymbol{packagePath: unescaped, name: name} +} + +// unescapePackagePath reverses the escaping cmd/internal/objabi.PathToPrefix applies to import paths in +// symbol names: bytes like '.' (at or after the final '/'), '%', '"', control bytes, and high bytes are +// written as lowercase "%xx". For example "gopkg.in/yaml.v2" is stored as "gopkg.in/yaml%2ev2", so this +// restores it before matching against the (unescaped) module paths from build info. A lone or malformed +// '%' sequence is left as-is. +func unescapePackagePath(path string) string { + if !strings.Contains(path, "%") { + return path + } + var b strings.Builder + b.Grow(len(path)) + for i := 0; i < len(path); i++ { + if path[i] == '%' && i+2 < len(path) { + if hi, ok1 := unhex(path[i+1]); ok1 { + if lo, ok2 := unhex(path[i+2]); ok2 { + b.WriteByte(hi<<4 | lo) + i += 2 + continue + } + } + } + b.WriteByte(path[i]) + } + return b.String() +} + +func unhex(c byte) (byte, bool) { + switch { + case c >= '0' && c <= '9': + return c - '0', true + case c >= 'a' && c <= 'f': + return c - 'a' + 10, true + case c >= 'A' && c <= 'F': + return c - 'A' + 10, true + } + return 0, false +} + // nameWithoutTypeArgs strips the type-argument portion from an instantiated generic symbol name, e.g. // "foo/bar.Do[net/url.Values]" -> "foo/bar.Do". The slashes and dots inside the brackets would otherwise // corrupt package-path derivation (yielding "foo/bar.Do[net" for the example above). Mirrors diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go index 72ca613da..5d140a431 100644 --- a/syft/pkg/cataloger/golang/symbols_test.go +++ b/syft/pkg/cataloger/golang/symbols_test.go @@ -97,6 +97,58 @@ func Test_moduleSymbols(t *testing.T) { } } +func Test_escapedImportPathAttribution(t *testing.T) { + // the go linker escapes the '.' in the last path element, so "gopkg.in/yaml.v2" appears in the symbol + // table as "gopkg.in/yaml%2ev2". attribution must still land these under the unescaped module path. + mainModule := &debug.Module{Path: "github.com/someorg/somecli"} + deps := []*debug.Module{{Path: "gopkg.in/yaml.v2"}} + + symbols := []binarySymbol{ + makeBinarySymbol("gopkg.in/yaml%2ev2.(*decoder).alias", "gopkg.in/yaml%2ev2"), + makeBinarySymbol("gopkg.in/yaml%2ev2.(*TypeError).Error", "gopkg.in/yaml%2ev2"), + } + + gotByModule, gotStdlib := moduleSymbols(symbols, mainModule, deps) + assert.Nil(t, gotStdlib) + assert.Equal(t, map[string]map[string][]string{ + "gopkg.in/yaml.v2": { + "gopkg.in/yaml.v2": {"(*TypeError).Error", "(*decoder).alias"}, + }, + }, gotByModule) +} + +func Test_unescapePackagePath(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"gopkg.in/yaml%2ev2", "gopkg.in/yaml.v2"}, + {"foo%25bar", "foo%bar"}, + {"foo%22bar", "foo\"bar"}, + // no escapes: unchanged + {"github.com/foo/bar", "github.com/foo/bar"}, + {"", ""}, + // malformed sequences are left as-is + {"foo%2", "foo%2"}, + {"foo%zz", "foo%zz"}, + {"foo%", "foo%"}, + // uppercase hex is accepted too + {"foo%2Ebar", "foo.bar"}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + assert.Equal(t, tt.expected, unescapePackagePath(tt.input)) + }) + } +} + +func Test_makeBinarySymbol(t *testing.T) { + // only the import-path prefix is unescaped; the local-symbol suffix is preserved verbatim + sym := makeBinarySymbol("gopkg.in/yaml%2ev2.(*decoder).alias", "gopkg.in/yaml%2ev2") + assert.Equal(t, "gopkg.in/yaml.v2", sym.packagePath) + assert.Equal(t, "gopkg.in/yaml.v2.(*decoder).alias", sym.name) +} + func Test_localSymbolName(t *testing.T) { tests := []struct { name string From 691b1357b2d11da21a8bbe26267f79552b969f15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:27:48 +0000 Subject: [PATCH 086/168] chore(deps): bump actions/checkout from 7.0.0 to 7.0.1 (#5095) Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- .github/workflows/validate-github-actions.yaml | 2 +- .github/workflows/validations.yaml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 35f09eeab..81370b372 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -75,7 +75,7 @@ jobs: packages: write # required for publishing release artifacts to GitHub packages id-token: write # required for keyless signing (cosign/sigstore OIDC) steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: fetch-depth: 0 persist-credentials: true diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index 6aaa5b82c..89788772d 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -20,7 +20,7 @@ jobs: contents: read security-events: write # for uploading SARIF results steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index 746319f30..b4bb10ec2 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -28,7 +28,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false @@ -50,7 +50,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false @@ -72,7 +72,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false @@ -105,7 +105,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false @@ -135,7 +135,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false @@ -179,7 +179,7 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false @@ -213,7 +213,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 with: persist-credentials: false From a6eedecd319229aa6fdab2e3ab593d54ddc24ee3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:29:04 +0000 Subject: [PATCH 087/168] chore(deps): bump github.com/gpustack/gguf-parser-go (#5097) Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.24.1 to 0.25.0. - [Release notes](https://github.com/gpustack/gguf-parser-go/releases) - [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.24.1...v0.25.0) --- updated-dependencies: - dependency-name: github.com/gpustack/gguf-parser-go dependency-version: 0.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 63d4e8378..e7702dc2d 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/google/licensecheck v0.3.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.1 - github.com/gpustack/gguf-parser-go v0.24.1 + github.com/gpustack/gguf-parser-go v0.25.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-getter v1.8.6 github.com/hashicorp/go-multierror v1.1.1 diff --git a/go.sum b/go.sum index 12fe1ee88..0b4e0ca5a 100644 --- a/go.sum +++ b/go.sum @@ -558,8 +558,8 @@ github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU= github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= -github.com/gpustack/gguf-parser-go v0.24.1 h1:nTYtL8HFK6ZhB90RKBu4oX2b3ZHpJLrMmKRfL9w9Cyc= -github.com/gpustack/gguf-parser-go v0.24.1/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= +github.com/gpustack/gguf-parser-go v0.25.0 h1:1AMBhMKtI24nTtn588Bq53FqNiOvEw1x9Nb4HbRrThs= +github.com/gpustack/gguf-parser-go v0.25.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc= From b38e40e3e675a7e53d59ed65781a6c68d256dfb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:29:21 +0000 Subject: [PATCH 088/168] chore(deps): bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0 (#5096) Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.7 to 0.6.0. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/192e21d79ab29983730a13d1382995c2307fbcaa...6599ee8b7a49aef6a770f63d261d214911a7ce02) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/validate-github-actions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index 89788772d..0ed9051a9 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: "Run zizmor" - uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 + uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0 with: # there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default) advanced-security: true From 2840ea6653ceb55807ff66427bfc8a1360f416a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:29:49 +0000 Subject: [PATCH 089/168] chore(deps): bump modernc.org/sqlite from 1.53.0 to 1.54.0 (#5098) Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.53.0 to 1.54.0. - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.53.0...v1.54.0) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.54.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e7702dc2d..bd6b3bbad 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.48.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.53.0 + modernc.org/sqlite v1.54.0 ) require ( @@ -324,7 +324,7 @@ require ( google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - modernc.org/libc v1.73.4 // indirect + modernc.org/libc v1.74.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 0b4e0ca5a..ac7240e01 100644 --- a/go.sum +++ b/go.sum @@ -1513,20 +1513,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= -modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= -modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= +modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= -modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= -modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= +modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1535,8 +1535,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= -modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= +modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 9af0098a6847b42eb7b48222935cb501ae61ea09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:48:24 +0000 Subject: [PATCH 090/168] chore(deps): bump google.golang.org/grpc from 1.80.0 to 1.82.1 (#5099) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.80.0 to 1.82.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.82.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.82.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index bd6b3bbad..aae9d3b51 100644 --- a/go.mod +++ b/go.mod @@ -118,7 +118,7 @@ require ( cloud.google.com/go/storage v1.61.3 // indirect dario.cat/mergo v1.0.2 // indirect github.com/DataDog/zstd v1.5.5 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -168,7 +168,7 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect github.com/containerd/containerd/api v1.11.1 // indirect github.com/containerd/containerd/v2 v2.3.3 // indirect @@ -189,8 +189,8 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/fgprof v0.9.5 // indirect @@ -299,7 +299,7 @@ require ( github.com/zclconf/go-cty v1.16.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect @@ -319,9 +319,9 @@ require ( gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/api v0.271.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/warnings.v0 v0.1.2 // indirect modernc.org/libc v1.74.1 // indirect diff --git a/go.sum b/go.sum index ac7240e01..55b03b1f5 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByX github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= @@ -290,8 +290,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= @@ -371,14 +371,14 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= -github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= -github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= -github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I= @@ -998,8 +998,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= @@ -1431,10 +1431,10 @@ google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1462,8 +1462,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From 8a229b16c4ac2a3d3666848886d97bbff7d2a40a Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 27 Jul 2026 10:04:27 -0400 Subject: [PATCH 091/168] chore(deps): update CPE dictionary index (#5109) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 121 +++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index b9385c447..cc31a1c8c 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -2412,6 +2412,9 @@ "@better-auth/oauth-provider": [ "cpe:2.3:a:better-auth:better-auth\\/oauth-provider:*:*:*:*:*:node.js:*:*" ], + "@better-auth/sso": [ + "cpe:2.3:a:better-auth:better-auth\\/sso:*:*:*:*:*:node.js:*:*" + ], "@braintree/sanitize-url": [ "cpe:2.3:a:paypal:braintree\\/sanitize-url:*:*:*:*:*:node.js:*:*" ], @@ -6837,6 +6840,9 @@ "pyanyapi": [ "cpe:2.3:a:pyanyapi_project:pyanyapi:*:*:*:*:*:*:*:*" ], + "pyasn1": [ + "cpe:2.3:a:pyasn1:pyasn1:*:*:*:*:*:python:*:*" + ], "pybluemonday": [ "cpe:2.3:a:python:pybluemonday:*:*:*:*:*:*:*:*" ], @@ -8557,6 +8563,9 @@ "add-subtitle": [ "cpe:2.3:a:viitorcloud:add_subtitle:*:*:*:*:*:wordpress:*:*" ], + "add-svg-support-for-media-uploader-inventivo": [ + "cpe:2.3:a:inventivo:inventivo:*:*:*:*:*:wordpress:*:*" + ], "add-to-any": [ "cpe:2.3:a:addtoany:addtoany_share_buttons:*:*:*:*:*:wordpress:*:*" ], @@ -8747,6 +8756,9 @@ "advanced-recent-posts": [ "cpe:2.3:a:advanced_recent_posts_project:advanced_recent_posts:*:*:*:*:*:wordpress:*:*" ], + "advanced-schedule-posts": [ + "cpe:2.3:a:hijiriworld:advanced_schedule_posts:*:*:*:*:*:wordpress:*:*" + ], "advanced-sermons": [ "cpe:2.3:a:wpcodeus:advanced_sermons:*:*:*:*:*:wordpress:*:*" ], @@ -8811,6 +8823,7 @@ "cpe:2.3:a:ai_contact_us_form_project:ai_contact_us_form:*:*:*:*:*:wordpress:*:*" ], "ai-engine": [ + "cpe:2.3:a:jordymeow:ai_engine:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:meowapps:ai_engine:*:*:*:*:*:wordpress:*:*" ], "ai-image-alt-text-generator-for-wp": [ @@ -11021,6 +11034,9 @@ "cost-calculator-builder": [ "cpe:2.3:a:stylemixthemes:cost_calculator_builder:*:*:*:*:free:wordpress:*:*" ], + "coub": [ + "cpe:2.3:a:chartscss:coub:*:*:*:*:*:wordpress:*:*" + ], "countdown-block": [ "cpe:2.3:a:wpdeveloper:countdown_block:*:*:*:*:*:wordpress:*:*" ], @@ -12150,9 +12166,12 @@ "cpe:2.3:a:mandsconsulting:email_before_download:*:*:*:*:*:wordpress:*:*" ], "email-encoder-bundle": [ - "cpe:2.3:a:jannisthuemmig:email_encoder:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:onlineoptimisation:email_encoder:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:wp-webhooks:email_encoder:*:*:*:*:*:wordpress:*:*" ], + "email-keep": [ + "cpe:2.3:a:intricateweb:email_keep:*:*:*:*:*:wordpress:*:*" + ], "email-log": [ "cpe:2.3:a:email_log_project:email_log:*:*:*:*:*:wordpress:*:*" ], @@ -13132,6 +13151,9 @@ "game-server-status": [ "cpe:2.3:a:game-server-status_project:game-server-status:*:*:*:*:*:wordpress:*:*" ], + "game-users-share-buttons": [ + "cpe:2.3:a:gameusers:game_users_share_button:*:*:*:*:*:wordpress:*:*" + ], "gamepress": [ "cpe:2.3:a:gamepress_project:gamepress:*:*:*:*:*:wordpress:*:*" ], @@ -13141,6 +13163,9 @@ "gamipress-link": [ "cpe:2.3:a:gamipress:gamipress_-_link:*:*:*:*:*:wordpress:*:*" ], + "gamipress-reset-user": [ + "cpe:2.3:a:gamipress:gamipress_-_reset_user:*:*:*:*:*:wordpress:*:*" + ], "garden-gnome-package": [ "cpe:2.3:a:ggnome:garden_gnome_package:*:*:*:*:*:wordpress:*:*" ], @@ -13225,6 +13250,9 @@ "gestpay-for-woocommerce": [ "cpe:2.3:a:fabrick:gestpay_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], + "get-bookings-wp": [ + "cpe:2.3:a:istmoplugins:get_bookings_wp:*:*:*:*:*:wordpress:*:*" + ], "get-custom-field-values": [ "cpe:2.3:a:get_custom_field_values_project:get_custom_field_values:*:*:*:*:*:wordpress:*:*" ], @@ -13255,6 +13283,15 @@ "gf-custom-style": [ "cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*" ], + "gf-hubspot": [ + "cpe:2.3:a:crmperks:wp_gravity_forms_hubspot:*:*:*:*:*:wordpress:*:*" + ], + "gf-insightly": [ + "cpe:2.3:a:crmperks:wp_gravity_forms_insightly:*:*:*:*:*:wordpress:*:*" + ], + "gf-salesforce-crmperks": [ + "cpe:2.3:a:crmperks:wp_gravity_forms_salesforce:*:*:*:*:*:wordpress:*:*" + ], "gf-zoho": [ "cpe:2.3:a:crmperks:wp_gravity_forms_zoho_crm_and_bigin:*:*:*:*:*:wordpress:*:*" ], @@ -13687,6 +13724,9 @@ "heureka": [ "cpe:2.3:a:heureka:heureka:*:*:*:*:*:wordpress:*:*" ], + "hide-links": [ + "cpe:2.3:a:avovkdesign:hide_links:*:*:*:*:*:wordpress:*:*" + ], "hide-my-wp": [ "cpe:2.3:a:wpplugins:hide_my_wp_ghost:*:*:*:*:*:wordpress:*:*" ], @@ -13711,6 +13751,9 @@ "hkdev-maintenance-mode": [ "cpe:2.3:a:helderk:maintenance_mode:*:*:*:*:*:wordpress:*:*" ], + "hmapsprem": [ + "cpe:2.3:a:heroplugins:hero_maps_premium:*:*:*:*:*:wordpress:*:*" + ], "hms-testimonials": [ "cpe:2.3:a:hitmyserver:hms_testimonials:*:*:*:*:*:wordpress:*:*" ], @@ -14085,6 +14128,9 @@ "cpe:2.3:a:infusionsoft_project:infusionsoft:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:katz:infusionsoft_gravity_forms:*:*:*:*:*:wordpress:*:*" ], + "infusionsoft-official-opt-in-forms": [ + "cpe:2.3:a:keap:keap_official_opt_in_forms:*:*:*:*:*:wordpress:*:*" + ], "inline-google-spreadsheet-viewer": [ "cpe:2.3:a:inline_google_spreadsheet_viewer_project:inline_google_spreadsheet_viewer:*:*:*:*:*:wordpress:*:*" ], @@ -14185,6 +14231,9 @@ "intelly-related-posts": [ "cpe:2.3:a:data443:inline_related_posts:*:*:*:*:*:wordpress:*:*" ], + "interact-quiz-embed": [ + "cpe:2.3:a:tryinteract:embed_a_quiz_on_your_site:*:*:*:*:*:wordpress:*:*" + ], "interactive-3d-flipbook-powered-physics-engine": [ "cpe:2.3:a:3dflipbook:3d_flipbook:*:*:*:*:*:wordpress:*:*" ], @@ -14421,6 +14470,9 @@ "jvm-rich-text-icons": [ "cpe:2.3:a:jorisvm:jvm_gutenberg_rich_text_icons:*:*:*:*:*:wordpress:*:*" ], + "jwp-a11y": [ + "cpe:2.3:a:jidaikobo:jwp-a11y:*:*:*:*:*:wordpress:*:*" + ], "kadence-blocks": [ "cpe:2.3:a:kadencewp:gutenberg_blocks_with_ai:*:*:*:*:*:wordpress:*:*" ], @@ -14910,6 +14962,9 @@ "logo-slider-wp": [ "cpe:2.3:a:logichunt:logo_slider:*:*:*:*:*:wordpress:*:*" ], + "logs-de-connexion": [ + "cpe:2.3:a:floriansimunek:connexion_logs:*:*:*:*:*:wordpress:*:*" + ], "logwpmail": [ "cpe:2.3:a:premierethemes:log_wp_mail:*:*:*:*:*:wordpress:*:*" ], @@ -14982,6 +15037,9 @@ "cpe:2.3:a:custom_popup_builder_project:custom_popup_builder:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:popup_\\|_custom_popup_builder_project:popup_\\|_custom_popup_builder:*:*:*:*:*:wordpress:*:*" ], + "magayo-lottery-results": [ + "cpe:2.3:a:magayo:magayo_lottery_results:*:*:*:*:*:wordpress:*:*" + ], "magazine-blocks": [ "cpe:2.3:a:themegrill:magazine_blocks:*:*:*:*:*:wordpress:*:*" ], @@ -15178,7 +15236,8 @@ "cpe:2.3:a:master-addons:master_addons:*:*:*:*:*:wordpress:*:*" ], "master-slider": [ - "cpe:2.3:a:averta:master_slider:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:averta:master_slider:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:averta:master_slider:*:*:*:*:free:wordpress:*:*" ], "masterstudy-lms-learning-management-system": [ "cpe:2.3:a:stylemixthemes:masterstudy_lms:*:*:*:*:*:wordpress:*:*" @@ -15198,6 +15257,9 @@ "materialis-companion": [ "cpe:2.3:a:extendthemes:materialis_companion:*:*:*:*:*:wordpress:*:*" ], + "matomo": [ + "cpe:2.3:a:matomo:matomo:*:*:*:*:*:wordpress:*:*" + ], "matrix-pre-loader": [ "cpe:2.3:a:nkb-bd:preloader_matrix:*:*:*:*:*:wordpress:*:*" ], @@ -15216,6 +15278,9 @@ "mdc-youtube-downloader": [ "cpe:2.3:a:mdc_youtube_downloader_project:mdc_youtube_downloader:*:*:*:*:*:wordpress:*:*" ], + "media-alt-renamer": [ + "cpe:2.3:a:jordymeow:media_alt_renamer:*:*:*:*:*:wordpress:*:*" + ], "media-downloader": [ "cpe:2.3:a:media_downloader_project:media_downloader:*:*:*:*:*:wordpress:*:*" ], @@ -15547,6 +15612,9 @@ "move-addons": [ "cpe:2.3:a:moveaddons:move_addons_for_elementor:*:*:*:*:*:wordpress:*:*" ], + "movie-database": [ + "cpe:2.3:a:heimkino-praxis:movie_database:*:*:*:*:*:wordpress:*:*" + ], "movies": [ "cpe:2.3:a:movies_project:movies:*:*:*:*:*:wordpress:*:*" ], @@ -16239,6 +16307,7 @@ "cpe:2.3:a:benhuson:page_parts:*:*:*:*:*:wordpress:*:*" ], "page-scroll-to-id": [ + "cpe:2.3:a:malihu:page_scroll_to_id:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:page_scroll_to_id_project:page_scroll_to_id:*:*:*:*:*:wordpress:*:*" ], "page-views-count": [ @@ -16672,6 +16741,9 @@ "popup-zyrex": [ "cpe:2.3:a:zyrex:popup:*:*:*:*:*:wordpress:*:*" ], + "popup4phone": [ + "cpe:2.3:a:ivanweb:popup4phone:*:*:*:*:*:wordpress:*:*" + ], "popupally": [ "cpe:2.3:a:accessally:popupally:*:*:*:*:*:wordpress:*:*" ], @@ -16685,6 +16757,9 @@ "portfolio": [ "cpe:2.3:a:bestwebsoft:portfolio:*:*:*:*:*:wordpress:*:*" ], + "portfolio-and-projects": [ + "cpe:2.3:a:essentialplugin:portfolio_and_projects:*:*:*:*:*:wordpress:*:*" + ], "portfolio-by-lisa-westlund": [ "cpe:2.3:a:portfolio_project:portfolio:*:*:*:*:*:wordpress:*:*" ], @@ -17472,6 +17547,9 @@ "responsive-css-editor": [ "cpe:2.3:a:wpwox:responsive_css_editor:*:*:*:*:*:wordpress:*:*" ], + "responsive-filterable-portfolio": [ + "cpe:2.3:a:i13websolution:responsive_filterable_portfolio:*:*:*:*:*:wordpress:*:*" + ], "responsive-food-and-drink-menu": [ "cpe:2.3:a:corporatezen:responsive_food_and_drink_menu:*:*:*:*:*:wordpress:*:*" ], @@ -17501,6 +17579,12 @@ "responsive-vector-maps": [ "cpe:2.3:a:thinkupthemes:responsive_vector_maps:*:*:*:*:*:wordpress:*:*" ], + "responsive-video-embed": [ + "cpe:2.3:a:lepileppanen:responsive_video_embed:*:*:*:*:*:wordpress:*:*" + ], + "rest-api-to-miniprogram": [ + "cpe:2.3:a:jianbo:rest_api_to_miniprogram:*:*:*:*:*:wordpress:*:*" + ], "restaurant-cafe-addon-for-elementor": [ "cpe:2.3:a:nicheaddons:restaurant_\\\u0026_cafe_addon_for_elementor:*:*:*:*:*:wordpress:*:*" ], @@ -17686,6 +17770,9 @@ "safe-svg": [ "cpe:2.3:a:10up:safe_svg:*:*:*:*:*:wordpress:*:*" ], + "safetymails-forms": [ + "cpe:2.3:a:henriquerodrigues:safetyforms:*:*:*:*:*:wordpress:*:*" + ], "sagepay-server-gateway-for-woocommerce": [ "cpe:2.3:a:patsatech:sagepay_server_gateway_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], @@ -18576,6 +18663,9 @@ "slideshow-se": [ "cpe:2.3:a:slideshow_se_project:slideshow_se:*:*:*:*:*:wordpress:*:*" ], + "slingblocks": [ + "cpe:2.3:a:funnelkit:slingblocks:*:*:*:*:*:wordpress:*:*" + ], "small-package-quotes-fedex-edition": [ "cpe:2.3:a:eniture:small_package_quotes:*:*:*:*:fedex:wordpress:*:*" ], @@ -18868,6 +18958,9 @@ "sportspress": [ "cpe:2.3:a:themeboy:sportspress:*:*:*:*:*:wordpress:*:*" ], + "spotbot": [ + "cpe:2.3:a:jakehelbig:spotbot:*:*:*:*:*:wordpress:*:*" + ], "spotify-play-button-for-wordpress": [ "cpe:2.3:a:followmedarling:spotify-play-button-for-wordpress:*:*:*:*:*:wordpress:*:*" ], @@ -19970,6 +20063,9 @@ "user-login-log": [ "cpe:2.3:a:intechnosoftware:user_login_log:*:*:*:*:*:wordpress:*:*" ], + "user-messages": [ + "cpe:2.3:a:marvinlabs:user_messages:*:*:*:*:*:wordpress:*:*" + ], "user-meta": [ "cpe:2.3:a:user-meta:user_meta_user_profile_builder_and_user_management:*:*:*:*:*:wordpress:*:*" ], @@ -19993,6 +20089,9 @@ "user-role": [ "cpe:2.3:a:bestwebsoft:user_role:*:*:*:*:*:wordpress:*:*" ], + "user-shortcodes-plus": [ + "cpe:2.3:a:kylebjohnson:user_shortcodes_plus:*:*:*:*:*:wordpress:*:*" + ], "user-spam-remover": [ "cpe:2.3:a:joelhardi:user_spam_remover:*:*:*:*:*:wordpress:*:*" ], @@ -20605,6 +20704,9 @@ "woo-gutenberg-products-block": [ "cpe:2.3:a:automattic:woocommerce_blocks:*:*:*:*:*:wordpress:*:*" ], + "woo-mailerlite": [ + "cpe:2.3:a:mailerlite:mailerlite:*:*:*:*:*:wordpress:*:*" + ], "woo-moneybird": [ "cpe:2.3:a:techastha:integration_of_moneybird_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], @@ -20954,6 +21056,9 @@ "wordpress-users": [ "cpe:2.3:a:jonathonkemp:wordpress_users:*:*:*:*:*:wordpress:*:*" ], + "work-the-flow-file-upload": [ + "cpe:2.3:a:lyntonreed:work_the_flow_file_upload:*:*:*:*:*:wordpress:*:*" + ], "workreap": [ "cpe:2.3:a:amentotech:workreap:*:*:*:*:*:wordpress:*:*" ], @@ -21185,6 +21290,9 @@ "wp-consultant": [ "cpe:2.3:a:wp_consultant_project:wp_consultant:*:*:-:*:-:wordpress:*:*" ], + "wp-contact-form": [ + "cpe:2.3:a:ftwr:wp_contact_form:*:*:*:*:*:wordpress:*:*" + ], "wp-contact-slider": [ "cpe:2.3:a:wpexperts:wp_contact_slider:*:*:*:*:*:wordpress:*:*" ], @@ -21483,6 +21591,9 @@ "wp-fountain": [ "cpe:2.3:a:wp_fountain_project:wp_fountain:*:*:*:*:*:wordpress:*:*" ], + "wp-front-end-login-and-register": [ + "cpe:2.3:a:hellomohsinkhan:wp_front-end_login_and_register:*:*:*:*:*:wordpress:*:*" + ], "wp-front-end-profile": [ "cpe:2.3:a:wp_front_end_profile_project:wp_front_end_profile:*:*:*:*:*:wordpress:*:*" ], @@ -21935,6 +22046,9 @@ "wp-product-gallery-lite": [ "cpe:2.3:a:accesspressthemes:wp_product_gallery_lite:*:*:*:*:*:wordpress:*:*" ], + "wp-programmmanager": [ + "cpe:2.3:a:mantus667:wp-pmanager:*:*:*:*:*:wordpress:*:*" + ], "wp-propagator": [ "cpe:2.3:a:widgilabs:plugin_propagator:*:*:*:*:*:wordpress:*:*" ], @@ -23143,6 +23257,9 @@ "empowerwp": [ "cpe:2.3:a:extendthemes:empowerwp:*:*:*:*:*:wordpress:*:*" ], + "enfold": [ + "cpe:2.3:a:kriesi:enfold:*:*:*:*:*:wordpress:*:*" + ], "enlighten": [ "cpe:2.3:a:accesspressthemes:enlighten:*:*:*:*:*:wordpress:*:*" ], From 138d9ce2a095755a04b06b7e7d9173c976502d9a Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Mon, 27 Jul 2026 10:18:50 -0400 Subject: [PATCH 092/168] added bun binary classifier (#5103) * added bun binary classifier Signed-off-by: Rez Moss * added bun binary classifier Signed-off-by: Rez Moss --------- Signed-off-by: Rez Moss --- syft/pkg/cataloger/binary/capabilities.yaml | 10 +++++ .../binary/classifier_cataloger_test.go | 44 +++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 14 ++++++ .../snippets/bun/0.5.9/linux-amd64/bun | 12 +++++ .../snippets/bun/1.0.0/linux-amd64/bun | 12 +++++ .../snippets/bun/1.1.0/linux-amd64/bun | 12 +++++ .../snippets/bun/1.3.5/linux-amd64/bun | 12 +++++ 7 files changed, 116 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml index 17f36be0b..e8d68b254 100644 --- a/syft/pkg/cataloger/binary/capabilities.yaml +++ b/syft/pkg/cataloger/binary/capabilities.yaml @@ -433,6 +433,16 @@ catalogers: cpes: - cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:* type: BinaryPkg + - method: glob + criteria: + - '**/bun' + packages: + - class: bun-binary + name: bun + purl: pkg:generic/bun + cpes: + - cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:* + type: BinaryPkg - method: glob criteria: - '**/ghc*' diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index 286699a76..b620a8674 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -1565,6 +1565,50 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("deno-binary"), }, }, + { + logicalFixture: "bun/0.5.9/linux-amd64", + expected: pkg.Package{ + Name: "bun", + Version: "0.5.9", + Type: "binary", + PURL: "pkg:generic/bun@0.5.9", + Locations: locations("bun"), + Metadata: metadata("bun-binary"), + }, + }, + { + logicalFixture: "bun/1.0.0/linux-amd64", + expected: pkg.Package{ + Name: "bun", + Version: "1.0.0", + Type: "binary", + PURL: "pkg:generic/bun@1.0.0", + Locations: locations("bun"), + Metadata: metadata("bun-binary"), + }, + }, + { + logicalFixture: "bun/1.1.0/linux-amd64", + expected: pkg.Package{ + Name: "bun", + Version: "1.1.0", + Type: "binary", + PURL: "pkg:generic/bun@1.1.0", + Locations: locations("bun"), + Metadata: metadata("bun-binary"), + }, + }, + { + logicalFixture: "bun/1.3.5/linux-amd64", + expected: pkg.Package{ + Name: "bun", + Version: "1.3.5", + Type: "binary", + PURL: "pkg:generic/bun@1.3.5", + Locations: locations("bun"), + Metadata: metadata("bun-binary"), + }, + }, { logicalFixture: "haskell-ghc/7.10.3/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 9c5adeea4..212f244a2 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -611,6 +611,20 @@ func DefaultClassifiers() []binutils.Classifier { PURL: mustPURL("pkg:generic/deno@version"), CPEs: singleCPE("cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, + { + Class: "bun-binary", + FileGlob: "**/bun", + EvidenceMatcher: binutils.MatchAny( + // bun 1.1.0 + // Bun v1.0.0 + m.FileContentsVersionMatcher(`(?m)[Bb]un v?(?P[0-9]+\.[0-9]+\.[0-9]+)`), + // bun-1.1.0 + m.FileContentsVersionMatcher(`(?m)bun-(?P[0-9]+\.[0-9]+\.[0-9]+)`), + ), + Package: "bun", + PURL: mustPURL("pkg:generic/bun@version"), + CPEs: singleCPE("cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*"), + }, { Class: "haskell-ghc-binary", FileGlob: "**/ghc*", diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun new file mode 100644 index 000000000..a19be8c22 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun @@ -0,0 +1,12 @@ +name: bun +offset: 1000000 +length: 100 +snippetSha256: 0000000000000000000000000000000000000000000000000000000000000001 +fileSha256: 0000000000000000000000000000000000000000000000000000000000000001 + +### byte snippet to follow ### +] + +Bun v0.5.9 +bun-0.5.9 +JavaScript runtime for developing, testing, and bundling diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun new file mode 100644 index 000000000..163f6bf84 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun @@ -0,0 +1,12 @@ +name: bun +offset: 1000000 +length: 100 +snippetSha256: 0000000000000000000000000000000000000000000000000000000000000002 +fileSha256: 0000000000000000000000000000000000000000000000000000000000000002 + +### byte snippet to follow ### +] + +Bun v1.0.0 +bun-1.0.0 +JavaScript runtime for developing, testing, and bundling diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun new file mode 100644 index 000000000..719498ef3 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun @@ -0,0 +1,12 @@ +name: bun +offset: 1000000 +length: 100 +snippetSha256: 0000000000000000000000000000000000000000000000000000000000000003 +fileSha256: 0000000000000000000000000000000000000000000000000000000000000003 + +### byte snippet to follow ### +] + +Bun v1.1.0 +bun-1.1.0 +JavaScript runtime for developing, testing, and bundling diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun new file mode 100644 index 000000000..e68db67f7 --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun @@ -0,0 +1,12 @@ +name: bun +offset: 1000000 +length: 100 +snippetSha256: 0000000000000000000000000000000000000000000000000000000000000004 +fileSha256: 0000000000000000000000000000000000000000000000000000000000000004 + +### byte snippet to follow ### +] + +Bun v1.3.5 +bun-1.3.5 +JavaScript runtime for developing, testing, and bundling From 12b8ba47fbdf146db769377559649b002810f37e Mon Sep 17 00:00:00 2001 From: Arpit Jain <3242828+arpitjain099@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:22:41 +0900 Subject: [PATCH 093/168] Fix inverted bounds check dropping every Erlang string with a backslash (#5110) parseErlangString advances past a backslash escape and then checks len(data) >= *i before reading the escaped byte. That condition is almost always true (it only turns false once *i runs off the end), so the intended out-of-range guard fires on the very first escape character it sees instead of only at EOF. Any rebar.lock or OTP resource file containing a backslash in a quoted string (a Windows git path, an escaped quote, anything) fails to parse and the whole file, and every package in it, gets dropped. Flip the comparison to *i >= len(data) so the guard only trips when the escape is genuinely truncated, and add a regression test for a string with an escaped quote. Signed-off-by: Arpit Jain --- syft/pkg/cataloger/erlang/erlang_parser.go | 2 +- syft/pkg/cataloger/erlang/erlang_parser_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/erlang/erlang_parser.go b/syft/pkg/cataloger/erlang/erlang_parser.go index ae22589b4..819c3bb41 100644 --- a/syft/pkg/cataloger/erlang/erlang_parser.go +++ b/syft/pkg/cataloger/erlang/erlang_parser.go @@ -156,7 +156,7 @@ func parseErlangString(data []byte, i *int) (erlangNode, error) { } if c == '\\' { *i++ - if len(data) >= *i { + if *i >= len(data) { return node(nil), fmt.Errorf("invalid escape without closed string at %d", *i) } c = data[*i] diff --git a/syft/pkg/cataloger/erlang/erlang_parser_test.go b/syft/pkg/cataloger/erlang/erlang_parser_test.go index adb031ddb..9b6950793 100644 --- a/syft/pkg/cataloger/erlang/erlang_parser_test.go +++ b/syft/pkg/cataloger/erlang/erlang_parser_test.go @@ -96,6 +96,11 @@ func Test_parseErlang(t *testing.T) { { foo, bar } ]}`, }, + { + name: "string with an escaped quote", + content: ` +{escaped, ["a\"b"]}`, + }, } for _, test := range tests { @@ -112,3 +117,14 @@ func Test_parseErlang(t *testing.T) { }) } } + +func Test_parseErlangString_escapedByte(t *testing.T) { + // a backslash escape mid-string used to always error out, since the bounds + // check at the escape site was inverted (len(data) >= *i is true for + // almost every position, not just an out-of-range one). + data := []byte(`"a\"b"`) + i := 0 + got, err := parseErlangString(data, &i) + require.NoError(t, err) + assert.Equal(t, `a"b`, got.String()) +} From 2dcf5163b8112c0029b7534df9b747db0e0c7b6a Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Mon, 27 Jul 2026 17:36:56 +0300 Subject: [PATCH 094/168] fix(apk): allow large installed db fields (#5100) Signed-off-by: cyphercodes Co-authored-by: cyphercodes --- syft/pkg/cataloger/alpine/parse_apk_db.go | 3 +++ .../pkg/cataloger/alpine/parse_apk_db_test.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/syft/pkg/cataloger/alpine/parse_apk_db.go b/syft/pkg/cataloger/alpine/parse_apk_db.go index 094d076f6..e661797a1 100644 --- a/syft/pkg/cataloger/alpine/parse_apk_db.go +++ b/syft/pkg/cataloger/alpine/parse_apk_db.go @@ -32,12 +32,15 @@ type parsedData struct { pkg.ApkDBEntry } +const maxApkDBFieldSize = 10 * 1024 * 1024 + // parseApkDB parses packages from a given APK "installed" flat-file DB. For more // information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec. // //nolint:funlen func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { scanner := bufio.NewScanner(reader) + scanner.Buffer(nil, maxApkDBFieldSize) var errs error var apks []parsedData diff --git a/syft/pkg/cataloger/alpine/parse_apk_db_test.go b/syft/pkg/cataloger/alpine/parse_apk_db_test.go index 0354a5931..ee146d723 100644 --- a/syft/pkg/cataloger/alpine/parse_apk_db_test.go +++ b/syft/pkg/cataloger/alpine/parse_apk_db_test.go @@ -719,6 +719,25 @@ func Test_processChecksum(t *testing.T) { } } +func TestParseApkDBAllowsLargeFieldValues(t *testing.T) { + contents := strings.Join([]string{ + "P:large-description", + "V:1.0-r0", + "A:x86_64", + "S:1", + "I:1", + "T:" + strings.Repeat("a", 70*1024), + "", + }, "\n") + reader := file.NewLocationReadCloser(file.NewLocation("large-installed-db"), io.NopCloser(strings.NewReader(contents))) + + pkgs, _, err := parseApkDB(context.Background(), nil, new(generic.Environment), reader) + + require.NoError(t, err) + require.Len(t, pkgs, 1) + assert.Equal(t, "large-description", pkgs[0].Name) +} + func Test_parseApkDB_expectedPkgNames(t *testing.T) { tests := []struct { fixture string From 86baeeb481ce7de42841993fb4424a696a89d2ee Mon Sep 17 00:00:00 2001 From: Enes Deniz <142517728+3nesdeniz@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:50:13 +0300 Subject: [PATCH 095/168] fix(rust): omit Cargo PURLs for local packages (#5105) Signed-off-by: Enes Deniz <142517728+3nesdeniz@users.noreply.github.com> --- syft/pkg/cataloger/rust/package.go | 14 +++- syft/pkg/cataloger/rust/package_test.go | 33 +++++++++ .../cataloger/rust/parse_cargo_lock_test.go | 73 ++++++++++++++++++- .../rust/testdata/path-deps/Cargo.lock | 21 ++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock diff --git a/syft/pkg/cataloger/rust/package.go b/syft/pkg/cataloger/rust/package.go index 9ed04696d..aebf04540 100644 --- a/syft/pkg/cataloger/rust/package.go +++ b/syft/pkg/cataloger/rust/package.go @@ -14,7 +14,7 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc Name: m.Name, Version: m.Version, Locations: file.NewLocationSet(locations...), - PURL: packageURL(m.Name, m.Version), + PURL: cargoLockPackageURL(m), Language: pkg.Rust, Type: pkg.RustPkg, Metadata: m, @@ -25,6 +25,18 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc return p } +func cargoLockPackageURL(m pkg.RustCargoLockEntry) string { + // Cargo omits source for packages resolved from the local workspace or a + // path dependency. A cargo PURL without a repository qualifier identifies + // a package on crates.io, so emitting one for a local package can cause + // downstream scanners to match unrelated registry vulnerabilities. + if m.Source == "" { + return "" + } + + return packageURL(m.Name, m.Version) +} + func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg.Package { p := pkg.Package{ Name: dep.Name, diff --git a/syft/pkg/cataloger/rust/package_test.go b/syft/pkg/cataloger/rust/package_test.go index 01f1f9a2f..c1cc84125 100644 --- a/syft/pkg/cataloger/rust/package_test.go +++ b/syft/pkg/cataloger/rust/package_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" ) func Test_packageURL(t *testing.T) { @@ -31,3 +33,34 @@ func Test_packageURL(t *testing.T) { }) } } + +func TestNewPackageFromCargoMetadataPURL(t *testing.T) { + tests := []struct { + name string + source string + want string + }{ + { + name: "crates.io package", + source: "registry+https://github.com/rust-lang/crates.io-index", + want: "pkg:cargo/telemetry@0.1.0", + }, + { + name: "local path package", + source: "", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newPackageFromCargoMetadata(pkg.RustCargoLockEntry{ + Name: "telemetry", + Version: "0.1.0", + Source: tt.source, + }) + + assert.Equal(t, tt.want, got.PURL) + }) + } +} diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go index aa13b3755..3b74e4b67 100644 --- a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go +++ b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go @@ -351,7 +351,7 @@ func TestCargoLockWithGitDependencies(t *testing.T) { helloWorld := pkg.Package{ Name: "hello_world", Version: "0.1.0", - PURL: "pkg:cargo/hello_world@0.1.0", + PURL: "", Locations: locations, Language: pkg.Rust, Type: pkg.RustPkg, @@ -673,6 +673,77 @@ func TestCargoLockWithGitDependencies(t *testing.T) { pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships) } +func TestCargoLockWithPathDependencies(t *testing.T) { + fixture := "testdata/path-deps/Cargo.lock" + locations := file.NewLocationSet(file.NewLocation(fixture)) + + app := pkg.Package{ + Name: "app", + Version: "1.0.0", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: pkg.NewLicenseSet(), + Metadata: pkg.RustCargoLockEntry{ + Name: "app", + Version: "1.0.0", + Dependencies: []string{ + "memchr", + "telemetry", + }, + }, + } + memchr := pkg.Package{ + Name: "memchr", + Version: "2.7.4", + PURL: "pkg:cargo/memchr@2.7.4", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: pkg.NewLicenseSet(), + Metadata: pkg.RustCargoLockEntry{ + Name: "memchr", + Version: "2.7.4", + Source: "registry+https://github.com/rust-lang/crates.io-index", + Checksum: "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3", + Dependencies: []string{}, + }, + } + telemetry := pkg.Package{ + Name: "telemetry", + Version: "0.1.0", + Locations: locations, + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: pkg.NewLicenseSet(), + Metadata: pkg.RustCargoLockEntry{ + Name: "telemetry", + Version: "0.1.0", + Dependencies: []string{}, + }, + } + + expectedPkgs := []pkg.Package{ + app, + memchr, + telemetry, + } + expectedRelationships := []artifact.Relationship{ + { + From: memchr, + To: app, + Type: artifact.DependencyOfRelationship, + }, + { + From: telemetry, + To: app, + Type: artifact.DependencyOfRelationship, + }, + } + + pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships) +} + func TestCargoLockDependencySpecification(t *testing.T) { tests := []struct { name string diff --git a/syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock b/syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock new file mode 100644 index 000000000..388954608 --- /dev/null +++ b/syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock @@ -0,0 +1,21 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "app" +version = "1.0.0" +dependencies = [ + "memchr", + "telemetry", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "telemetry" +version = "0.1.0" From 295454945ca6b725d73b44da9b1dc263a91b878b Mon Sep 17 00:00:00 2001 From: Eljees <57435526+Eljees@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:18:27 +0300 Subject: [PATCH 096/168] fix: strip publisher URL from RPM CPE vendor (#5081) Signed-off-by: Eljees --- .../pkg/cataloger/internal/cpegenerate/rpm.go | 49 +++++++++++---- .../internal/cpegenerate/rpm_test.go | 60 +++++++++++++++++++ 2 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 syft/pkg/cataloger/internal/cpegenerate/rpm_test.go diff --git a/syft/pkg/cataloger/internal/cpegenerate/rpm.go b/syft/pkg/cataloger/internal/cpegenerate/rpm.go index ecaea15f5..54a1e8c42 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/rpm.go +++ b/syft/pkg/cataloger/internal/cpegenerate/rpm.go @@ -1,26 +1,49 @@ package cpegenerate -import "github.com/anchore/syft/syft/pkg" +import ( + "net/url" + "strings" + + "github.com/anchore/syft/syft/pkg" +) func candidateVendorsForRPM(p pkg.Package) fieldCandidateSet { vendors := newFieldCandidateSet() + var vendor string switch m := p.Metadata.(type) { case pkg.RpmDBEntry: - if m.Vendor != "" { - vendors.add(fieldCandidate{ - value: normalizeName(m.Vendor), - disallowSubSelections: true, - }) - } + vendor = m.Vendor case pkg.RpmArchive: - if m.Vendor != "" { - vendors.add(fieldCandidate{ - value: normalizeName(m.Vendor), - disallowSubSelections: true, - }) - } + vendor = m.Vendor + } + + vendor = stripTrailingURL(vendor) + if vendor != "" { + vendors.add(fieldCandidate{ + value: normalizeName(vendor), + disallowSubSelections: true, + }) } return vendors } + +func stripTrailingURL(value string) string { + trimmed := strings.TrimSpace(value) + if !strings.HasSuffix(trimmed, ">") { + return value + } + + open := strings.LastIndex(trimmed, "<") + if open == -1 { + return value + } + + parsed, err := url.Parse(trimmed[open+1 : len(trimmed)-1]) + if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { + return value + } + + return strings.TrimSpace(trimmed[:open]) +} diff --git a/syft/pkg/cataloger/internal/cpegenerate/rpm_test.go b/syft/pkg/cataloger/internal/cpegenerate/rpm_test.go new file mode 100644 index 000000000..e5dc059d3 --- /dev/null +++ b/syft/pkg/cataloger/internal/cpegenerate/rpm_test.go @@ -0,0 +1,60 @@ +package cpegenerate + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" +) + +func TestCandidateVendorsForRPM(t *testing.T) { + tests := []struct { + name string + metadata any + expected []string + }{ + { + name: "database vendor with publisher URL", + metadata: pkg.RpmDBEntry{ + Vendor: "SUSE LLC ", + }, + expected: []string{"susellc"}, + }, + { + name: "archive vendor with publisher URL", + metadata: pkg.RpmArchive{ + Vendor: "SUSE LLC ", + }, + expected: []string{"susellc"}, + }, + { + name: "plain vendor", + metadata: pkg.RpmDBEntry{ + Vendor: "Red Hat, Inc.", + }, + expected: []string{"redhat"}, + }, + { + name: "non-URL angle-bracket suffix", + metadata: pkg.RpmDBEntry{ + Vendor: "Example ", + }, + expected: []string{"example"}, + }, + { + name: "non-HTTP URL", + metadata: pkg.RpmDBEntry{ + Vendor: "Example ", + }, + expected: []string{"example"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := pkg.Package{Metadata: test.metadata} + assert.ElementsMatch(t, test.expected, candidateVendorsForRPM(p).uniqueValues()) + }) + } +} From 12866894191e3e9508cfb92a04b0ffcf1bd41f19 Mon Sep 17 00:00:00 2001 From: Eljees <57435526+Eljees@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:18:48 +0300 Subject: [PATCH 097/168] Fix missing nested packages in package-lock.json v1 (#5108) * fix(javascript): catalog nested package-lock v1 dependencies Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com> * test(javascript): cover nested package-lock v1 dependencies Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com> --------- Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com> --- .../javascript/parse_package_lock.go | 44 ++++++++++---- .../javascript/parse_package_lock_test.go | 60 +++++++++++++++++++ .../pkg-lock/nested-package-lock-1.json | 54 +++++++++++++++++ 3 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 syft/pkg/cataloger/javascript/testdata/pkg-lock/nested-package-lock-1.json diff --git a/syft/pkg/cataloger/javascript/parse_package_lock.go b/syft/pkg/cataloger/javascript/parse_package_lock.go index 3ac451c0d..4d12410ae 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock.go @@ -27,10 +27,11 @@ type packageLock struct { // lockDependency represents a single package dependency listed in the package.lock json file type lockDependency struct { - Version string `json:"version"` - Resolved string `json:"resolved"` - Integrity string `json:"integrity"` - Dev bool `json:"dev"` + Version string `json:"version"` + Resolved string `json:"resolved"` + Integrity string `json:"integrity"` + Dev bool `json:"dev"` + Dependencies map[string]lockDependency `json:"dependencies"` } type lockPackage struct { @@ -77,14 +78,7 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve } if lock.LockfileVersion == 1 { - for name, pkgMeta := range lock.Dependencies { - // skip packages that are only present as a dev dependency - if !a.cfg.IncludeDevDependencies && pkgMeta.Dev { - continue - } - - pkgs = append(pkgs, newPackageLockV1Package(ctx, a.cfg, resolver, reader.Location, name, pkgMeta)) - } + pkgs = append(pkgs, a.packageLockV1Packages(ctx, resolver, reader.Location, lock.Dependencies)...) } if lock.LockfileVersion == 2 || lock.LockfileVersion == 3 { @@ -116,6 +110,32 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve return pkgs, dependency.Resolve(packageLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages") } +func (a genericPackageLockAdapter) packageLockV1Packages(ctx context.Context, resolver file.Resolver, location file.Location, dependencies map[string]lockDependency) []pkg.Package { + var pkgs []pkg.Package + seen := make(map[string]struct{}) + + var walk func(map[string]lockDependency) + walk = func(dependencies map[string]lockDependency) { + for name, pkgMeta := range dependencies { + // Skipping a dev-only dependency also skips its dev-only subtree. + if !a.cfg.IncludeDevDependencies && pkgMeta.Dev { + continue + } + + p := newPackageLockV1Package(ctx, a.cfg, resolver, location, name, pkgMeta) + if _, exists := seen[p.PURL]; !exists { + pkgs = append(pkgs, p) + seen[p.PURL] = struct{}{} + } + + walk(pkgMeta.Dependencies) + } + } + + walk(dependencies) + return pkgs +} + func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) { // The license field could be either a string or an array. diff --git a/syft/pkg/cataloger/javascript/parse_package_lock_test.go b/syft/pkg/cataloger/javascript/parse_package_lock_test.go index 35a51b394..c7fc55144 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock_test.go @@ -111,6 +111,66 @@ func TestParsePackageLock(t *testing.T) { pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, expectedRelationships) } +func TestParsePackageLockV1NestedDependencies(t *testing.T) { + fixture := "testdata/pkg-lock/nested-package-lock-1.json" + expectedPkgs := []pkg.Package{ + { + Name: "duplicate", + Version: "1.0.0", + PURL: "pkg:npm/duplicate@1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz", Integrity: "sha512-duplicate"}, + }, + { + Name: "middle", + Version: "1.0.0", + PURL: "pkg:npm/middle@1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/middle/-/middle-1.0.0.tgz", Integrity: "sha512-middle"}, + }, + { + Name: "parent", + Version: "1.0.0", + PURL: "pkg:npm/parent@1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/parent/-/parent-1.0.0.tgz", Integrity: "sha512-parent"}, + }, + { + Name: "shared", + Version: "0.5.0", + PURL: "pkg:npm/shared@0.5.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-0.5.0.tgz", Integrity: "sha512-shared-0.5"}, + }, + { + Name: "shared", + Version: "1.0.0", + PURL: "pkg:npm/shared@1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-1.0.0.tgz", Integrity: "sha512-shared-1"}, + }, + { + Name: "shared", + Version: "2.0.0", + PURL: "pkg:npm/shared@2.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-2.0.0.tgz", Integrity: "sha512-shared-2"}, + }, + } + for i := range expectedPkgs { + expectedPkgs[i].Locations.Add(file.NewLocation(fixture)) + } + + adapter := newGenericPackageLockAdapter(CatalogerConfig{}) + pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, nil) +} + func TestParsePackageLockV2(t *testing.T) { ctx := context.TODO() fixture := "testdata/pkg-lock/package-lock-2.json" diff --git a/syft/pkg/cataloger/javascript/testdata/pkg-lock/nested-package-lock-1.json b/syft/pkg/cataloger/javascript/testdata/pkg-lock/nested-package-lock-1.json new file mode 100644 index 000000000..51d25701f --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/pkg-lock/nested-package-lock-1.json @@ -0,0 +1,54 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "parent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent/-/parent-1.0.0.tgz", + "integrity": "sha512-parent", + "dependencies": { + "shared": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shared/-/shared-1.0.0.tgz", + "integrity": "sha512-shared-1" + }, + "middle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/middle/-/middle-1.0.0.tgz", + "integrity": "sha512-middle", + "dependencies": { + "shared": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/shared/-/shared-0.5.0.tgz", + "integrity": "sha512-shared-0.5" + } + } + }, + "duplicate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz", + "integrity": "sha512-duplicate" + } + } + }, + "shared": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shared/-/shared-2.0.0.tgz", + "integrity": "sha512-shared-2" + }, + "duplicate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz", + "integrity": "sha512-duplicate" + }, + "dev-parent": { + "version": "1.0.0", + "dev": true, + "dependencies": { + "dev-nested": { + "version": "1.0.0" + } + } + } + } +} From 16223e6dd7893fe578787658ceb876257483d404 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 27 Jul 2026 15:20:49 -0400 Subject: [PATCH 098/168] fix: consider vendored golang packages in module attribution (#5093) Signed-off-by: Keith Zantow --- syft/pkg/cataloger/golang/symbols.go | 53 +++++++++++++++---- syft/pkg/cataloger/golang/symbols_test.go | 62 +++++++++++++++++++++++ 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go index 3b0987f2e..cd6b8bdd3 100644 --- a/syft/pkg/cataloger/golang/symbols.go +++ b/syft/pkg/cataloger/golang/symbols.go @@ -16,6 +16,10 @@ import ( // mainPackage is the import path the linker assigns to the binary's main package. const mainPackage = "main" +// vendorPrefix is the import-path prefix carried by vendored packages (e.g. from `go mod vendor`, or the +// standard library's own vendored dependencies such as "vendor/golang.org/x/net/http2"). +const vendorPrefix = "vendor/" + // binarySymbol represents a single function symbol extracted from a go binary's pclntab. type binarySymbol struct { // packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz") @@ -319,8 +323,13 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) { // "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main" // import path the linker assigns. Standard-library symbols (which belong to no module) are collected // separately and returned as the second value, grouped by import path, so they can be attached to the -// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a -// recognizable stdlib import path are dropped. +// synthetic "stdlib" package. Vendored packages carry a "vendor/" import-path prefix: such symbols match +// both modules whose own path carries the prefix and modules without it (matched with the prefix trimmed), +// and the prefix is retained in the group key only when the owning module itself is named "vendor/...". +// Module-less vendored packages (the stdlib's own vendored dependencies, e.g. +// "vendor/golang.org/x/net/http2") are dropped: stdlib vulnerabilities seem to be reported against the public +// packages (e.g. "crypto/x509"), not the vendored internal copies. Compiler/runtime-internal symbols that +// are neither module-owned nor a recognizable stdlib import path are likewise dropped. func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) { if len(symbols) == 0 { return nil, nil @@ -348,16 +357,17 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod attrPath = main.Path } - var best string - for _, modPath := range modulePaths { - if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) { - best = modPath - } + best := findBestMatch(modulePaths, attrPath) + + // the vendor/ prefix is only retained when the owning module itself is named "vendor/..."; + // in all other cases (non-vendored modules and vendored stdlib) the recorded import path is trimmed + if !strings.HasPrefix(best, vendorPrefix) { + importPath = strings.TrimPrefix(importPath, vendorPrefix) } local := localSymbolName(sym.name, importPath) if best == "" { - if importPath != mainPackage && isStandardImportPath(importPath) { + if importPath != mainPackage && isStandardImportPath(importPath) { // drop stdlib vendored packages stdlib[importPath] = append(stdlib[importPath], local) } continue @@ -379,11 +389,36 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod return results, stdlib } +// findBestMatch returns the module path that owns the given package path taking into account vendor/ prefixes: a vendor/ import path +// will take precedence and continue to match, non-vendored imports will match against their vendored equivalent +func findBestMatch(modulePaths []string, importPath string) string { + trimmedPath, trimmed := strings.CutPrefix(importPath, vendorPrefix) + candidatePaths := []string{importPath, trimmedPath} + if !trimmed { + candidatePaths = candidatePaths[:1] + } + + var best string + for _, candidate := range candidatePaths { + for _, modPath := range modulePaths { + // the prefix must end at a path-segment boundary in candidate, so that e.g. the module + // "github.com/foo/bar" matches the package "github.com/foo/bar/baz" but not "github.com/foo/barbaz" + if len(modPath) > len(best) && strings.HasPrefix(candidate, modPath) && (candidate == modPath || candidate[len(modPath)] == '/') { + best = modPath + } + } + } + return best +} + // localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g. // "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned // unchanged when it does not carry the expected prefix. func localSymbolName(name, importPath string) string { - if importPath != "" && strings.HasPrefix(name, importPath+".") { + if !strings.HasPrefix(importPath, vendorPrefix) { + name = strings.TrimPrefix(name, vendorPrefix) + } + if len(importPath) < len(name) && strings.HasPrefix(name, importPath) && name[len(importPath)] == '.' { return name[len(importPath)+1:] } return name diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go index 5d140a431..bff5a778c 100644 --- a/syft/pkg/cataloger/golang/symbols_test.go +++ b/syft/pkg/cataloger/golang/symbols_test.go @@ -16,6 +16,8 @@ func Test_moduleSymbols(t *testing.T) { deps := []*debug.Module{ {Path: "github.com/foo/bar"}, {Path: "github.com/foo/bar/v2"}, + {Path: "vendor/github.com/vendored/mod"}, + {Path: "github.com/vendored/mod"}, nil, } @@ -74,6 +76,63 @@ func Test_moduleSymbols(t *testing.T) { "runtime": {"main"}, }, }, + { + name: "vendored packages match non-vendored modules and are recorded under the canonical import path", + symbols: []binarySymbol{ + {packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"}, + {packagePath: "vendor/github.com/foo/bar/internal/util", name: "vendor/github.com/foo/bar/internal/util.(*Helper).Do"}, + }, + expected: map[string]map[string][]string{ + "github.com/foo/bar": { + "github.com/foo/bar": {"Parse"}, + "github.com/foo/bar/internal/util": {"(*Helper).Do"}, + }, + }, + }, + { + name: "stdlib-vendored packages are dropped", + symbols: []binarySymbol{ + {packagePath: "vendor/golang.org/x/net/http2", name: "vendor/golang.org/x/net/http2.(*Framer).ReadFrame"}, + }, + expected: map[string]map[string][]string{}, + }, + { + name: "modules whose own path carries the vendor/ prefix match exactly and win over the trimmed match", + symbols: []binarySymbol{ + {packagePath: "vendor/github.com/vendored/mod", name: "vendor/github.com/vendored/mod.Run"}, + {packagePath: "github.com/vendored/mod", name: "github.com/vendored/mod.Run"}, + }, + expected: map[string]map[string][]string{ + "vendor/github.com/vendored/mod": { + "vendor/github.com/vendored/mod": {"Run"}, + }, + "github.com/vendored/mod": { + "github.com/vendored/mod": {"Run"}, + }, + }, + }, + { + name: "vendored and unvendored symbols for the same package are merged and deduplicated under the canonical import path", + symbols: []binarySymbol{ + {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"}, + {packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"}, + }, + expected: map[string]map[string][]string{ + "github.com/foo/bar": { + "github.com/foo/bar": {"Parse"}, + }, + }, + }, + { + // "github.com/foo/barbaz" shares a string prefix with the "github.com/foo/bar" module but not a + // path-segment boundary, so it must not be attributed to it (and, having a dotted first element, + // it is not a stdlib path either — it is dropped) + name: "module paths only match the package path at a path-segment boundary", + symbols: []binarySymbol{ + {packagePath: "github.com/foo/barbaz", name: "github.com/foo/barbaz.Parse"}, + }, + expected: map[string]map[string][]string{}, + }, { name: "duplicate symbols are deduplicated", symbols: []binarySymbol{ @@ -291,6 +350,9 @@ func Test_packagePathFromSymbolName(t *testing.T) { // module paths that begin with "go." are not compiler-generated {"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"}, {"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"}, + // vendored packages retain their "vendor/" import-path prefix in symbol names + {"vendor/golang.org/x/net/http2.(*Framer).ReadFrame", "vendor/golang.org/x/net/http2"}, + {"vendor/github.com/foo/bar.Parse", "vendor/github.com/foo/bar"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { From 476ce60768a86d77c4d286561ca430dd01621e4d Mon Sep 17 00:00:00 2001 From: Yashas Gunderia <68075205+ychampion@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:03:46 +0530 Subject: [PATCH 099/168] fix(binary): detect Deno 1.11 and 2.9 versions Signed-off-by: ychampion Co-authored-by: ychampion --- .../binary/classifier_cataloger_test.go | 33 ++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 9 +++++ .../snippets/deno/1.11.3/linux-amd64/deno | 9 +++++ .../snippets/deno/1.11.4/linux-amd64/deno | 9 +++++ .../snippets/deno/2.9.2/linux-amd64/deno | Bin 0 -> 349 bytes .../pkg/cataloger/binary/testdata/config.yaml | 24 +++++++++++++ 6 files changed, 84 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.3/linux-amd64/deno create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.4/linux-amd64/deno create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/2.9.2/linux-amd64/deno diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index b620a8674..8a44fc13a 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -1499,6 +1499,28 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("deno-binary"), }, }, + { + logicalFixture: "deno/1.11.3/linux-amd64", + expected: pkg.Package{ + Name: "deno", + Version: "1.11.3", + Type: "binary", + PURL: "pkg:generic/deno@1.11.3", + Locations: locations("deno"), + Metadata: metadata("deno-binary"), + }, + }, + { + logicalFixture: "deno/1.11.4/linux-amd64", + expected: pkg.Package{ + Name: "deno", + Version: "1.11.4", + Type: "binary", + PURL: "pkg:generic/deno@1.11.4", + Locations: locations("deno"), + Metadata: metadata("deno-binary"), + }, + }, { logicalFixture: "deno/1.16.4/linux-amd64", expected: pkg.Package{ @@ -1565,6 +1587,17 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("deno-binary"), }, }, + { + logicalFixture: "deno/2.9.2/linux-amd64", + expected: pkg.Package{ + Name: "deno", + Version: "2.9.2", + Type: "binary", + PURL: "pkg:generic/deno@2.9.2", + Locations: locations("deno"), + Metadata: metadata("deno-binary"), + }, + }, { logicalFixture: "bun/0.5.9/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 212f244a2..afd4c7536 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -588,11 +588,20 @@ func DefaultClassifiers() []binutils.Classifier { Class: "deno-binary", FileGlob: "**/deno", EvidenceMatcher: binutils.MatchAny( + m.FileContentsVersionMatcher( + // Deno/2.6.3Deno/ + // Deno/1.41.0cli/ + `Deno/(?P[0-9]+\.[0-9]+\.[0-9]+)(Deno/|cli/)`, + ), m.FileContentsVersionMatcher( // Deno/2.6.3 // Deno/1.41.0 `Deno/(?P[0-9]+\.[0-9]+\.[0-9]+)`, ), + m.FileContentsVersionMatcher( + // cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlwindows + `deno-canary/[0-9a-z]{40}(?P[0-9]+\.[0-9]+\.[0-9]+)`, + ), m.FileContentsVersionMatcher( // deno::tools::standalonedeno-65db94feba9d4d51a09b74629f566dbc90484fbarelease/v1.29.4windows // cli/tools/standalone.rsdeno-74064c9d8c222b33b2a552ea0af1054f57002a96release/v1.28.3windows diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.3/linux-amd64/deno b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.3/linux-amd64/deno new file mode 100644 index 000000000..448f0f90e --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.3/linux-amd64/deno @@ -0,0 +1,9 @@ +name: deno +offset: 4221667 +length: 120 +snippetSha256: 89509d15b73fd2d901b20ebfe82c67fd27f70e7b01dcbf376e7e80e6c80f97d9 +fileSha256: 530a95c93ddfa41bd4d72d665e677d5af9a4275f1520e638305e60af5083a6d6 + +### byte snippet to follow ### +g ctrl+d or close() +Error: Uncaught cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlw \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.4/linux-amd64/deno b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.4/linux-amd64/deno new file mode 100644 index 000000000..4d8d16dbd --- /dev/null +++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/1.11.4/linux-amd64/deno @@ -0,0 +1,9 @@ +name: deno +offset: 4225763 +length: 120 +snippetSha256: b47e8cf36bdf93e4bc63a2d7bd0a5d759975f12751a66908661777eba7e90025 +fileSha256: 1a55c23bb0b666d1b26d9d16f3a9d27210b6b13421031f7f18ff18abfcb8664f + +### byte snippet to follow ### +g ctrl+d or close() +Error: Uncaught cli/tools/standalone.rsdeno-canary/ed16f4332263e81bf00a39ebc2200291b1fcbb8e1.11.4dlw \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/2.9.2/linux-amd64/deno b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/deno/2.9.2/linux-amd64/deno new file mode 100644 index 0000000000000000000000000000000000000000..44ce7dbe4c0fe32945de7de28469ffeb01957127 GIT binary patch literal 349 zcmYL^K~4iP3`JRI%LO8Ji8Lg3;v}=?23!C;acHGsrfNr^?zkE^W2joO`+EP&dY<`1 zN85B>%+mW=*UHNO#N9)0v^L#iSb$#74L>jH@+ImtT^)dJ6K%4+l3e+V9 zpM$hWoC;*G-PaT=sUz=Vf`CoP^uwh0V^M16#DNJik`pYxHRTH0xu&MID5VW=TGTRn z>0WawE;)+PCp2ca+gZ7+YHyl$U2I>bY5B5SWmqPyJoD*ts?&Ogp$zc%@~57z`3WOc skMOdw=PUV7_wF{)MsBkyf+1pL$eVXTUywPA5COV literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml index 3bab3a99c..9e6197353 100644 --- a/syft/pkg/cataloger/binary/testdata/config.yaml +++ b/syft/pkg/cataloger/binary/testdata/config.yaml @@ -140,6 +140,22 @@ from-images: paths: - /usr/bin/deno + - name: deno + version: 1.11.3 + images: + - ref: denoland/deno:1.11.3@sha256:340d071dc71c8403c75713694db04f59b8cb7f6c7244798cd7447d55f2750596 + platform: linux/amd64 + paths: + - /usr/bin/deno + + - name: deno + version: 1.11.4 + images: + - ref: denoland/deno:1.11.4@sha256:8e9a449a50b71c779ad1c10ec79055b20a331e78a3852818fac47bc3e85b49f4 + platform: linux/amd64 + paths: + - /usr/bin/deno + - name: deno version: 1.16.4 images: @@ -188,6 +204,14 @@ from-images: paths: - /usr/bin/deno + - name: deno + version: 2.9.2 + images: + - ref: denoland/deno:ubuntu-2.9.2@sha256:0d27941f871700ebd95953c36fb1a817135c20f5afe3a645935672dfab72b2e9 + platform: linux/amd64 + paths: + - /usr/bin/deno + # haskell:7.10.3 detail information is missing. - name: haskell-ghc version: 7.10.3 From b236f8c1f703cca1ec154505a9a7821ad7c62605 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:30:07 +0000 Subject: [PATCH 100/168] chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.8.1 to 6.8.2 (#5051) Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.8.1 to 6.8.2. - [Release notes](https://github.com/jedib0t/go-pretty/releases) - [Commits](https://github.com/jedib0t/go-pretty/compare/v6.8.1...v6.8.2) --- updated-dependencies: - dependency-name: github.com/jedib0t/go-pretty/v6 dependency-version: 6.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index aae9d3b51..1c789c699 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/iancoleman/strcase v0.3.0 github.com/invopop/jsonschema v0.14.0 - github.com/jedib0t/go-pretty/v6 v6.8.1 + github.com/jedib0t/go-pretty/v6 v6.8.2 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 github.com/klauspost/compress v1.19.0 diff --git a/go.sum b/go.sum index 55b03b1f5..41d08cf8e 100644 --- a/go.sum +++ b/go.sum @@ -624,8 +624,8 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= -github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.2 h1:FmKNr1GOyot/zqNQplE8HLhFguJaeHJTCArntnI4uxE= +github.com/jedib0t/go-pretty/v6 v6.8.2/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= From 5ef5b1badc6542acae8bd9e1a81e5a7bab3a2d61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:33:37 +0000 Subject: [PATCH 101/168] chore(deps): bump anchore/workflows/.github/workflows/check-version-available.yaml (#5033) Bumps [anchore/workflows/.github/workflows/check-version-available.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0. - [Release notes](https://github.com/anchore/workflows/releases) - [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4) --- updated-dependencies: - dependency-name: anchore/workflows/.github/workflows/check-version-available.yaml dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 81370b372..281e421da 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,7 +32,7 @@ jobs: if: ${{ github.event.inputs.phase == 'all' }} permissions: contents: read # required for fetching tags - uses: anchore/workflows/.github/workflows/check-version-available.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2 + uses: anchore/workflows/.github/workflows/check-version-available.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0 with: version: ${{ github.event.inputs.version }} From 08e913a2cec30b206bed5c4cc06d7a58a1fb6381 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 29 Jul 2026 13:50:43 -0400 Subject: [PATCH 102/168] Report each hardlink as its own file when scanning images (#5029) * report each hardlink as its own file when scanning images image scans previously collapsed a set of hardlinks onto a single file, so only one path per inode showed up in results. dir scans report every hardlink path, which made image vs dir SBOMs of the same filesystem diverge (and produce different SPDX `packageVerificationCode` values for packages that own hardlinked files). now both image resolvers (squash and all-layers) surface each hardlink at its own path as a regular file bound to the target's content, matching dir scans. user-facing impact: - SBOMs for images containing hardlinks will list more `file` entries - SPDX `packageVerificationCode` values change for affected packages, now matching the equivalent `dir:` scan - adds `file.NewVirtualLocationFromImage` to the public API fixes #5019 Signed-off-by: Alex Goodman * fix busybox test assertion Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- syft/file/location.go | 25 ++++ .../container_image_all_layers.go | 114 ++++++++++++------ .../fileresolver/container_image_squash.go | 60 ++++++++- .../container_image_squash_test.go | 85 +++++++++++++ .../testdata/image-hardlinks/Dockerfile | 12 ++ .../binary/classifier_cataloger_test.go | 4 +- 6 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 syft/internal/fileresolver/testdata/image-hardlinks/Dockerfile diff --git a/syft/file/location.go b/syft/file/location.go index ad12c8f49..f5e1aa51c 100644 --- a/syft/file/location.go +++ b/syft/file/location.go @@ -152,6 +152,31 @@ func NewLocationFromImage(accessPath string, ref file.Reference, img *image.Imag } } +// NewVirtualLocationFromImage creates a new Location surfaced at realPath but whose contents and metadata are +// described by the given reference. This is used to present a hardlink as the underlying type it points to (at the +// hardlink's own path) so that image results are in parity with directory results, which cannot distinguish a +// hardlink from a regular file. Note that RealPath (the hardlink's own path) intentionally differs from +// ref.RealPath (the target's path); callers must not assume the two agree for these locations. +func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location { + // the FileSystemID comes from the target ref's layer. a hardlink and its target are always materialized in the + // same layer tar (a tar hardlink entry can only reference a file within the same archive), so the target's layer + // digest is also the hardlink path's layer. + layer := img.FileCatalog.Layer(ref) + return Location{ + LocationData: LocationData{ + Coordinates: Coordinates{ + RealPath: realPath, + FileSystemID: layer.Metadata.Digest, + }, + AccessPath: accessPath, + ref: ref, + }, + LocationMetadata: LocationMetadata{ + Annotations: map[string]string{}, + }, + } +} + // NewLocationFromDirectory creates a new Location representing the given path (extracted from the Reference) relative to the given directory. func NewLocationFromDirectory(responsePath string, fd string, ref file.Reference) Location { return Location{ diff --git a/syft/internal/fileresolver/container_image_all_layers.go b/syft/internal/fileresolver/container_image_all_layers.go index ad7e9e0d3..20e8136bc 100644 --- a/syft/internal/fileresolver/container_image_all_layers.go +++ b/syft/internal/fileresolver/container_image_all_layers.go @@ -53,8 +53,20 @@ func (r *ContainerImageAllLayers) HasPath(path string) bool { return false } -func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqueFileIDs stereoscopeFile.ReferenceSet, layerIdx int) ([]stereoscopeFile.Reference, error) { - uniqueFiles := make([]stereoscopeFile.Reference, 0) +func (r *ContainerImageAllLayers) locationsByRef(ref stereoscopeFile.Reference, accessPath string, uniqueFileIDs stereoscopeFile.ReferenceSet, layerPos int) ([]file.Location, error) { + uniqueLocations := make([]file.Location, 0) + + // if the access path is itself a hardlink, surface it as the underlying type it points to, at its own path + // (bound to the target's content), so that image results are in parity with directory results (which cannot + // tell a hardlink from a regular file). the path-based lookup is required because the search that produced ref + // already followed the basename link, collapsing the hardlink onto its target. + if ownRef, targetRef, ok := r.hardLinkAtPath(accessPath, r.layers[layerPos]); ok { + if !uniqueFileIDs.Contains(ownRef) { + uniqueFileIDs.Add(ownRef) + uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), accessPath, targetRef, r.img)) + } + return uniqueLocations, nil + } // since there is potentially considerable work for each symlink/hardlink that needs to be resolved, let's check to see if this is a symlink/hardlink first entry, err := r.img.FileCatalog.Get(ref) @@ -65,22 +77,41 @@ func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqu if entry.Type == stereoscopeFile.TypeHardLink || entry.Type == stereoscopeFile.TypeSymLink { // a link may resolve in this layer or higher, assuming a squashed tree is used to search // we should search all possible resolutions within the valid source - for _, subLayerIdx := range r.layers[layerIdx:] { + for _, subLayerIdx := range r.layers[layerPos:] { resolvedRef, err := r.img.ResolveLinkByLayerSquash(ref, subLayerIdx) if err != nil { return nil, fmt.Errorf("failed to resolve link from layer (layer=%d ref=%+v): %w", subLayerIdx, ref, err) } if resolvedRef.HasReference() && !uniqueFileIDs.Contains(*resolvedRef.Reference) { uniqueFileIDs.Add(*resolvedRef.Reference) - uniqueFiles = append(uniqueFiles, *resolvedRef.Reference) + uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, *resolvedRef.Reference, r.img)) } } } else if !uniqueFileIDs.Contains(ref) { uniqueFileIDs.Add(ref) - uniqueFiles = append(uniqueFiles, ref) + uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, ref, r.img)) } - return uniqueFiles, nil + return uniqueLocations, nil +} + +// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a +// hardlink within the given layer. ok is false when path does not exist there or is not a hardlink. The lookup does +// not follow the basename link so that the hardlink's own path is preserved. This adds a tree walk per +// matched ref (path x layer); if it shows up in profiles, fold the hardlink check into the existing search resolution. +func (r *ContainerImageAllLayers) hardLinkAtPath(path string, layerIdx int) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) { + var own stereoscopeFile.Reference + // use the squashed-to-layer view (not just this layer's additions) so a hardlink added in a lower layer is still + // detected when the path is searched at a higher layer; otherwise it would collapse onto its target there. + exists, resolution, err := r.img.Layers[layerIdx].SquashedTree.File(stereoscopeFile.Path(path)) + if err != nil || !exists || !resolution.HasReference() { + return own, own, false + } + target, ok := r.resolveHardLinkTarget(*resolution.Reference, layerIdx) + if !ok { + return own, own, false + } + return *resolution.Reference, target, true } // FilesByPath returns all file.References that match the given paths from any layer in the image. @@ -112,14 +143,13 @@ func (r *ContainerImageAllLayers) FilesByPath(paths ...string) ([]file.Location, } } - results, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx) + locations, err := r.locationsByRef(*ref.Reference, path, uniqueFileIDs, idx) if err != nil { return nil, err } - for _, result := range results { - l := file.NewLocationFromImage(path, result, r.img) - r.annotateLocation(&l) - uniqueLocations = append(uniqueLocations, l) + for i := range locations { + r.annotateLocation(&locations[i]) + uniqueLocations = append(uniqueLocations, locations[i]) } } } @@ -158,14 +188,13 @@ func (r *ContainerImageAllLayers) FilesByGlob(patterns ...string) ([]file.Locati } } - refResults, err := r.fileByRef(*result.Reference, uniqueFileIDs, idx) + locations, err := r.locationsByRef(*result.Reference, string(result.RequestPath), uniqueFileIDs, idx) if err != nil { return nil, err } - for _, refResult := range refResults { - l := file.NewLocationFromImage(string(result.RequestPath), refResult, r.img) - r.annotateLocation(&l) - uniqueLocations = append(uniqueLocations, l) + for i := range locations { + r.annotateLocation(&locations[i]) + uniqueLocations = append(uniqueLocations, locations[i]) } } } @@ -233,14 +262,13 @@ func (r *ContainerImageAllLayers) FilesByMIMEType(types ...string) ([]file.Locat continue } - refResults, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx) + locations, err := r.locationsByRef(*ref.Reference, string(ref.RequestPath), uniqueFileIDs, idx) if err != nil { return nil, err } - for _, refResult := range refResults { - l := file.NewLocationFromImage(string(ref.RequestPath), refResult, r.img) - r.annotateLocation(&l) - uniqueLocations = append(uniqueLocations, l) + for i := range locations { + r.annotateLocation(&locations[i]) + uniqueLocations = append(uniqueLocations, locations[i]) } } } @@ -256,6 +284,11 @@ func (r *ContainerImageAllLayers) AllLocations(ctx context.Context) <-chan file. tree := r.img.Layers[layerIdx].Tree for _, ref := range tree.AllFiles(stereoscopeFile.AllTypes()...) { l := file.NewLocationFromImage(string(ref.RealPath), ref, r.img) + // surface a hardlink as the underlying type it points to (at its own path) so image results match + // directory results, which cannot distinguish a hardlink from a regular file. + if targetRef, ok := r.resolveHardLinkTarget(ref, layerIdx); ok { + l = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img) + } r.annotateLocation(&l) select { case <-ctx.Done(): @@ -273,6 +306,21 @@ func (r *ContainerImageAllLayers) FileMetadataByLocation(location file.Location) return fileMetadataByLocation(r.img, location) } +// resolveHardLinkTarget returns the reference of a hardlink's underlying target (resolved relative to the given layer) +// when ref is a hardlink; ok is false otherwise. No resolution is performed for non-hardlinks (symlinks keep their +// existing resolution semantics). +func (r *ContainerImageAllLayers) resolveHardLinkTarget(ref stereoscopeFile.Reference, layerIdx int) (stereoscopeFile.Reference, bool) { + metadata, err := r.img.FileCatalog.Get(ref) + if err != nil || metadata.Type != stereoscopeFile.TypeHardLink { + return ref, false + } + resolved, err := r.img.ResolveLinkByLayerSquash(ref, layerIdx) + if err != nil || !resolved.HasReference() { + return ref, false + } + return *resolved.Reference, true +} + func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) { if !r.markVisibility || l == nil { return @@ -282,26 +330,24 @@ func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) { annotation := file.VisibleAnnotation // if we find a location for a path that matches the query (e.g. **/node_modules) but is not present in the squashed tree, skip it - ref, err := r.img.SquashedSearchContext.SearchByPath(l.RealPath, filetree.DoNotFollowDeadBasenameLinks) - if err != nil || !ref.HasReference() { - annotation = file.HiddenAnnotation - } else if ref.ID() != givenRef.ID() { - // we may have the path in the squashed tree, but this must not be in the same layer + if !r.pathResolvesToRef(l.RealPath, givenRef) { annotation = file.HiddenAnnotation } // not only should the real path to the file exist, but the way we took to get there should also exist // (e.g. if we are looking for /etc/passwd, but the real path is /etc/passwd -> /etc/passwd-1, then we should // make certain that /etc/passwd-1 exists) - if annotation == file.VisibleAnnotation && l.AccessPath != "" { - ref, err := r.img.SquashedSearchContext.SearchByPath(l.AccessPath, filetree.DoNotFollowDeadBasenameLinks) - if err != nil || !ref.HasReference() { - annotation = file.HiddenAnnotation - } else if ref.ID() != givenRef.ID() { - // we may have the path in the squashed tree, but this must not be in the same layer - annotation = file.HiddenAnnotation - } + if annotation == file.VisibleAnnotation && l.AccessPath != "" && !r.pathResolvesToRef(l.AccessPath, givenRef) { + annotation = file.HiddenAnnotation } l.Annotations[file.VisibleAnnotationKey] = annotation } + +// pathResolvesToRef reports whether the given path in the squashed tree resolves to the given reference. +// SearchByPath always follows basename links, so a hardlink surfaced at its own path (whose reference is its +// target) still resolves to the target here and is correctly considered visible. +func (r *ContainerImageAllLayers) pathResolvesToRef(path string, target stereoscopeFile.Reference) bool { + ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.DoNotFollowDeadBasenameLinks) + return err == nil && ref.HasReference() && ref.ID() == target.ID() +} diff --git a/syft/internal/fileresolver/container_image_squash.go b/syft/internal/fileresolver/container_image_squash.go index efcd2e8e4..ef7122af6 100644 --- a/syft/internal/fileresolver/container_image_squash.go +++ b/syft/internal/fileresolver/container_image_squash.go @@ -40,6 +40,17 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er uniqueLocations := make([]file.Location, 0) for _, path := range paths { + // if the requested path is itself a hardlink, surface it at its own path (bound to its target's content) + // rather than collapsing it onto the target's path, so that image results are in parity with directory + // results (which cannot tell a hardlink from a regular file). + if ownRef, targetRef, ok := r.hardLinkAtPath(path); ok { + if !uniqueFileIDs.Contains(ownRef) { + uniqueFileIDs.Add(ownRef) + uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), path, targetRef, r.img)) + } + continue + } + ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.FollowBasenameLinks) if err != nil { return nil, err @@ -82,7 +93,7 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er // //nolint:gocognit func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location, error) { - uniqueFileIDs := stereoscopeFile.NewFileReferenceSet() + uniqueCoordinates := file.NewCoordinateSet() uniqueLocations := make([]file.Location, 0) for _, pattern := range patterns { @@ -116,10 +127,14 @@ func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location, return nil, fmt.Errorf("failed to find files by path (result=%+v): %w", result, err) } for _, resolvedLocation := range resolvedLocations { - if uniqueFileIDs.Contains(resolvedLocation.Reference()) { + // dedup on the surfaced coordinate rather than the underlying reference: distinct hardlinks share a + // single target reference but each has its own real path, so a reference-based dedup would collapse + // them back onto one entry (the exact behavior this parity fix removes). symlink resolutions keep + // their target's real path, so they still collapse as before. + if uniqueCoordinates.Contains(resolvedLocation.Coordinates) { continue } - uniqueFileIDs.Add(resolvedLocation.Reference()) + uniqueCoordinates.Add(resolvedLocation.Coordinates) uniqueLocations = append(uniqueLocations, resolvedLocation) } } @@ -179,10 +194,16 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc go func() { defer close(results) for _, ref := range r.img.SquashedTree().AllFiles(stereoscopeFile.AllTypes()...) { + loc := file.NewLocationFromImage(string(ref.RealPath), ref, r.img) + // surface a hardlink as the underlying type it points to (at its own path) so image results match + // directory results, which cannot distinguish a hardlink from a regular file. + if targetRef, ok := r.resolveHardLinkTarget(ref); ok { + loc = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img) + } select { case <-ctx.Done(): return - case results <- file.NewLocationFromImage(string(ref.RealPath), ref, r.img): + case results <- loc: continue } } @@ -190,6 +211,37 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc return results } +// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a +// hardlink. ok is false when path does not exist or is not a hardlink. The lookup does not follow the basename link so +// that the hardlink's own path is preserved. This adds a tree walk per FilesByPath path; if it shows up in +// profiles, fold the hardlink check into the existing SearchByPath resolution. +func (r *ContainerImageSquash) hardLinkAtPath(path string) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) { + var own stereoscopeFile.Reference + exists, resolution, err := r.img.SquashedTree().File(stereoscopeFile.Path(path)) + if err != nil || !exists || !resolution.HasReference() { + return own, own, false + } + target, ok := r.resolveHardLinkTarget(*resolution.Reference) + if !ok { + return own, own, false + } + return *resolution.Reference, target, true +} + +// resolveHardLinkTarget returns the reference of a hardlink's underlying target when ref is a hardlink; ok is false +// otherwise. No resolution is performed for non-hardlinks (symlinks keep their existing resolution semantics). +func (r *ContainerImageSquash) resolveHardLinkTarget(ref stereoscopeFile.Reference) (stereoscopeFile.Reference, bool) { + metadata, err := r.img.FileCatalog.Get(ref) + if err != nil || metadata.Type != stereoscopeFile.TypeHardLink { + return ref, false + } + resolved, err := r.img.ResolveLinkByImageSquash(ref) + if err != nil || !resolved.HasReference() { + return ref, false + } + return *resolved.Reference, true +} + func (r *ContainerImageSquash) FilesByMIMEType(types ...string) ([]file.Location, error) { refs, err := r.img.SquashedSearchContext.SearchByMIMEType(types...) if err != nil { diff --git a/syft/internal/fileresolver/container_image_squash_test.go b/syft/internal/fileresolver/container_image_squash_test.go index 4c1df13a6..82b0b87fb 100644 --- a/syft/internal/fileresolver/container_image_squash_test.go +++ b/syft/internal/fileresolver/container_image_squash_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/file" ) @@ -574,3 +575,87 @@ func TestSquashResolver_AllLocations(t *testing.T) { assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List())) } + +// a hardlink should be surfaced at its own path as the underlying type it points to (a regular file with the target's +// content and metadata), so that image results are in parity with directory results (which cannot distinguish a +// hardlink from a regular file). this must hold for both the squashed and all-layers resolvers. +func TestImageResolvers_Hardlinks(t *testing.T) { + img := imagetest.GetFixtureImage(t, "docker-archive", "image-hardlinks") + + resolvers := map[string]file.Resolver{} + squash, err := NewFromContainerImageSquash(img) + require.NoError(t, err) + resolvers["squashed"] = squash + + allLayers, err := NewFromContainerImageAllLayers(img) + require.NoError(t, err) + resolvers["all-layers"] = allLayers + + const wantContents = "hardlinked contents\n" + + // asserts a location surfaces the hardlink at its own path as a regular file with the target's content, and is + // not marked hidden (all-layers annotates visibility; squash leaves the annotation unset, which must not read as + // hidden either). + assertHardlink := func(t *testing.T, resolver file.Resolver, wantPath string, loc file.Location) { + t.Helper() + assert.Equal(t, wantPath, loc.RealPath, "expected the hardlink's own path, not the target's") + assert.NotEqual(t, file.HiddenAnnotation, loc.Annotations[file.VisibleAnnotationKey], "path=%s should not be hidden", wantPath) + + meta, err := resolver.FileMetadataByLocation(loc) + require.NoError(t, err) + assert.Equal(t, stereoscopeFile.TypeRegular, meta.Type, "path=%s", wantPath) + + reader, err := resolver.FileContentsByLocation(loc) + require.NoError(t, err) + actual, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Equal(t, wantContents, string(actual), "path=%s", wantPath) + } + + for name, resolver := range resolvers { + t.Run(name, func(t *testing.T) { + t.Run("FilesByPath surfaces a hardlink at its own path with the target's content", func(t *testing.T) { + for _, path := range []string{"/hardlink-a", "/hardlink-b", "/file.txt"} { + locs, err := resolver.FilesByPath(path) + require.NoError(t, err) + require.Len(t, locs, 1, "path=%s", path) + assertHardlink(t, resolver, path, locs[0]) + } + }) + + t.Run("FilesByGlob surfaces every hardlink in a matched set, not just one", func(t *testing.T) { + // the whole point of #5019: a glob that matches multiple hardlinks to the same target must return + // all of them (each at its own path), not collapse them onto a single entry. + locs, err := resolver.FilesByGlob("**/hardlink-*") + require.NoError(t, err) + + byPath := map[string]file.Location{} + for _, loc := range locs { + _, dup := byPath[loc.RealPath] + assert.Falsef(t, dup, "path %s emitted more than once", loc.RealPath) + byPath[loc.RealPath] = loc + } + for _, path := range []string{"/hardlink-a", "/hardlink-b"} { + loc, ok := byPath[path] + require.Truef(t, ok, "expected glob to surface %s", path) + assertHardlink(t, resolver, path, loc) + } + }) + + t.Run("AllLocations includes every hardlink path as a regular file exactly once", func(t *testing.T) { + counts := map[string]int{} + locsByPath := map[string]file.Location{} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + for loc := range resolver.AllLocations(ctx) { + counts[loc.RealPath]++ + locsByPath[loc.RealPath] = loc + } + for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} { + require.Equalf(t, 1, counts[path], "expected %s to be reported exactly once by AllLocations", path) + assertHardlink(t, resolver, path, locsByPath[path]) + } + }) + }) + } +} diff --git a/syft/internal/fileresolver/testdata/image-hardlinks/Dockerfile b/syft/internal/fileresolver/testdata/image-hardlinks/Dockerfile new file mode 100644 index 000000000..b733fb777 --- /dev/null +++ b/syft/internal/fileresolver/testdata/image-hardlinks/Dockerfile @@ -0,0 +1,12 @@ +# LAYER 0: +FROM busybox:1.34.0@sha256:e8e5cca392e3cf056fcdb3093e7ac2bf83fcf28b3bcf5818fe8ae71cf360c231 + +# LAYER 1: a regular file with two hardlinks to it, all created in the same layer so the layer tar contains one +# regular file entry and two hardlink (tar TypeLink) entries pointing at it. +RUN echo "hardlinked contents" > /file.txt && ln /file.txt /hardlink-a && ln /file.txt /hardlink-b + +# squash representation (of the files added here; busybox base also contributes hardlinked applets under /bin) +# . +# ├── file.txt +# ├── hardlink-a (hardlink -> file.txt) +# └── hardlink-b (hardlink -> file.txt) diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index 8a44fc13a..4cbd2f6f3 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -3069,8 +3069,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) { Name: "busybox", Version: "1.35.0", PURL: "pkg:generic/busybox@1.35.0", - Locations: locations("/bin/["), - Metadata: metadata("busybox-binary", "/bin/[", "/bin/busybox"), + Locations: locations("/bin/busybox"), + Metadata: metadata("busybox-binary", "/bin/busybox"), }, }, } From 31a352d03050b9ea4c36a4908419473c144629c9 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:50:20 -0400 Subject: [PATCH 103/168] fix(snap): release temp directories on every snap failure path --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/source/snapsource/manifest.go | 15 +- syft/source/snapsource/snap.go | 21 ++- syft/source/snapsource/snap_source.go | 55 +++++- syft/source/snapsource/snap_source_test.go | 210 +++++++++++++++++++++ syft/source/snapsource/snap_test.go | 83 ++++++++ 5 files changed, 365 insertions(+), 19 deletions(-) diff --git a/syft/source/snapsource/manifest.go b/syft/source/snapsource/manifest.go index 8d6c61611..a2d0b04d2 100644 --- a/syft/source/snapsource/manifest.go +++ b/syft/source/snapsource/manifest.go @@ -1,6 +1,7 @@ package snapsource import ( + "errors" "fmt" "github.com/goccy/go-yaml" @@ -21,6 +22,11 @@ type snapManifest struct { const manifestLocation = "/meta/snap.yaml" +// errNoManifest means the payload is a readable squashfs that simply does not carry a snap manifest. +// This is distinct from a manifest that is present but unusable, and callers are expected to treat +// the two differently. +var errNoManifest = errors.New("no snap manifest file found") + func parseManifest(resolver file.Resolver) (*snapManifest, error) { locations, err := resolver.FilesByPath(manifestLocation) if err != nil { @@ -28,7 +34,7 @@ func parseManifest(resolver file.Resolver) (*snapManifest, error) { } if len(locations) == 0 { - return nil, fmt.Errorf("no snap manifest file found") + return nil, errNoManifest } if len(locations) > 1 { @@ -48,9 +54,8 @@ func parseManifest(resolver file.Resolver) (*snapManifest, error) { return nil, fmt.Errorf("unable to decode snap manifest file: %w", err) } - if manifest.Name == "" || manifest.Version == "" { - return nil, fmt.Errorf("invalid snap manifest file: missing name or version") - } - + // note: name and version are deliberately not validated here. a manifest that decoded but is + // missing them still carries usable fields (summary, base, grade, ...) and throwing it away would + // describe less than we know. the caller surfaces the missing identity. return &manifest, nil } diff --git a/syft/source/snapsource/snap.go b/syft/source/snapsource/snap.go index c32ed86b6..0cf413a0d 100644 --- a/syft/source/snapsource/snap.go +++ b/syft/source/snapsource/snap.go @@ -77,22 +77,33 @@ func getRemoteSnapFile(ctx context.Context, fs afero.Fs, getter intFile.Getter, return newSnapFileFromRemote(ctx, fs, cfg, getter, info) } -func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (*snapFile, error) { +func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (_ *snapFile, err error) { t, err := afero.TempDir(fs, "", "syft-snap-") if err != nil { return nil, fmt.Errorf("failed to create temp directory: %w", err) } + closer := func() error { + return fs.RemoveAll(t) + } + + // any failure past this point must not leave the temp directory (or a partially downloaded snap) + // behind. On success the caller owns the directory by way of snapFile.Cleanup. + defer func() { + if err == nil { + return + } + if closeErr := closer(); closeErr != nil { + log.WithFields("error", closeErr, "directory", t).Warn("unable to remove temp directory for snap") + } + }() + snapFilePath := path.Join(t, path.Base(info.URL)) err = downloadSnap(getter, info, snapFilePath) if err != nil { return nil, fmt.Errorf("failed to download snap file: %w", err) } - closer := func() error { - return fs.RemoveAll(t) - } - mimeType, digests, err := getSnapFileInfo(ctx, fs, snapFilePath, cfg.DigestAlgorithms) if err != nil { return nil, err diff --git a/syft/source/snapsource/snap_source.go b/syft/source/snapsource/snap_source.go index 6710615d1..962e57e3c 100644 --- a/syft/source/snapsource/snap_source.go +++ b/syft/source/snapsource/snap_source.go @@ -3,6 +3,7 @@ package snapsource import ( "context" "crypto" + "errors" "fmt" "io" "os" @@ -119,7 +120,21 @@ func newFromPath(cfg Config, f *snapFile) (source.Source, error) { closer: f.Cleanup, } - return s, s.extractManifest() + if err := s.extractManifest(); err != nil { + // It is tried on any path ending in .snap or .squashfs, so declining one is routine and + // a later provider usually succeeds. When the user asked for --from snap there is no + // fallback and this error reaches them intact. + log.WithFields("error", err, "path", f.Path).Debug("unable to read snap") + + // never hand back a source alongside an error: callers that discard the source on error would + // leave behind the open squashfs file and the temp directory holding a downloaded snap + if closeErr := s.Close(); closeErr != nil { + log.WithFields("error", closeErr, "path", f.Path).Warn("unable to clean up snap source") + } + return nil, err + } + + return s, nil } func (s *snapSource) extractManifest() error { @@ -128,13 +143,26 @@ func (s *snapSource) extractManifest() error { return fmt.Errorf("unable to create snap file resolver: %w", err) } + // a manifest problem must never fail the source. this source is the only provider that can descend + // into squashfs (filesource cannot), so returning an error here would fall through to a provider + // that reports zero packages for a payload that has plenty. describe what we can instead. manifest, err := parseManifest(r) - if err != nil { - return fmt.Errorf("unable to parse snap manifest file: %w", err) + switch { + case errors.Is(err, errNoManifest): + // not every squashfs payload is a snap, and one without a manifest is still worth cataloging + log.WithFields("path", s.squashfsPath).Debug("no snap manifest file found") + return nil + case err != nil: + // the manifest is there but unusable, which is worth surfacing: the SBOM will have no source + // name or version to show for it + log.WithFields("error", err, "path", s.squashfsPath).Warn("unable to parse snap manifest file") + return nil } - if manifest != nil { - s.manifest = *manifest + s.manifest = *manifest + + if s.manifest.Name == "" || s.manifest.Version == "" { + log.WithFields("path", s.squashfsPath).Warn("snap manifest is missing name or version") } return nil } @@ -176,25 +204,34 @@ func (s snapSource) Describe() source.Description { } } +// Close releases everything the source holds. Every step runs even if an earlier one fails, since +// bailing early would skip the temp directory removal and leave a downloaded snap on disk. Safe to +// call more than once. func (s *snapSource) Close() error { + s.mutex.Lock() + defer s.mutex.Unlock() + + var errs error if s.squashFileCloser != nil { if err := s.squashFileCloser(); err != nil { - return fmt.Errorf("unable to close snap resolver: %w", err) + errs = errors.Join(errs, fmt.Errorf("unable to close snap resolver: %w", err)) } s.squashFileCloser = nil } s.resolver = nil if s.fs != nil { if err := s.fs.Close(); err != nil { - return fmt.Errorf("unable to close snap squashfs: %w", err) + errs = errors.Join(errs, fmt.Errorf("unable to close snap squashfs: %w", err)) } + s.fs = nil } if s.closer != nil { if err := s.closer(); err != nil { - return fmt.Errorf("unable to close snap source: %w", err) + errs = errors.Join(errs, fmt.Errorf("unable to close snap source: %w", err)) } + s.closer = nil } - return nil + return errs } func (s *snapSource) FileResolver(_ source.Scope) (file.Resolver, error) { diff --git a/syft/source/snapsource/snap_source_test.go b/syft/source/snapsource/snap_source_test.go index 99e8da7dc..4515f9c47 100644 --- a/syft/source/snapsource/snap_source_test.go +++ b/syft/source/snapsource/snap_source_test.go @@ -2,15 +2,23 @@ package snapsource import ( "crypto" + "errors" "fmt" + "os" + "path/filepath" + "strings" + "sync" "testing" + diskFile "github.com/diskfs/go-diskfs/backend/file" + "github.com/diskfs/go-diskfs/filesystem/squashfs" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/go-homedir" "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/source" ) func TestNewFromLocal(t *testing.T) { @@ -84,3 +92,205 @@ func TestNewFromLocal(t *testing.T) { }) } } + +func TestNewFromPathOnResolverError(t *testing.T) { + tests := []struct { + name string + // remote snaps carry a cleanup closure for the temp directory holding the download; local snaps + // have none, since the file belongs to the user + remote bool + wantCleanupRuns int + wantFileRemoved bool + }{ + { + name: "downloaded snap is cleaned up", + remote: true, + wantCleanupRuns: 1, + wantFileRemoved: true, + }, + { + name: "local snap file is left in place", + remote: false, + wantCleanupRuns: 0, + wantFileRemoved: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // the resolver reads the squashfs from the real filesystem, so the payload must live on disk + tempDir := t.TempDir() + snapPath := filepath.Join(tempDir, "not-really-a-snap.snap") + require.NoError(t, os.WriteFile(snapPath, []byte("not squashfs"), 0600)) + + var cleanupRuns int + f := &snapFile{ + Path: snapPath, + MimeType: "text/plain", + } + if tt.remote { + f.Cleanup = func() error { + cleanupRuns++ + return os.RemoveAll(tempDir) + } + } + + got, err := newFromPath(Config{Request: snapPath}, f) + + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to create snap file resolver") + + // a source returned alongside an error is discarded by callers without being closed, which + // would leave the downloaded snap behind + assert.Nil(t, got, "expected nil source on error") + assert.Equal(t, tt.wantCleanupRuns, cleanupRuns) + + _, statErr := os.Stat(snapPath) + if tt.wantFileRemoved { + assert.True(t, os.IsNotExist(statErr), "snap file was not cleaned up") + } else { + assert.NoError(t, statErr, "the user's snap file must never be removed") + } + }) + } +} + +// a manifest problem must never fail the source: snapsource is the only provider that can descend +// into squashfs, so an error here falls through to filesource and every package inside the payload +// silently disappears from the SBOM. +func TestNewFromPathToleratesManifestProblems(t *testing.T) { + tests := []struct { + name string + manifest string // written to /meta/snap.yaml; empty means no manifest at all + wantName string + wantVersion string + wantSummary string + }{ + { + name: "no manifest at all", + manifest: "", + }, + { + name: "manifest is not valid yaml", + manifest: "name: etcd\n\tversion: 3.4\n", + }, + { + name: "manifest is missing name and version", + manifest: "summary: a thing\nbase: core22\n", + wantSummary: "a thing", + }, + { + name: "manifest is complete", + manifest: "name: etcd\nversion: 3.4.0\nsummary: a thing\n", + wantName: "etcd", + wantVersion: "3.4.0", + wantSummary: "a thing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + snapPath := writeSquashfs(t, tt.manifest) + + src, err := newFromPath(Config{Request: snapPath}, &snapFile{Path: snapPath}) + require.NoError(t, err, "a manifest problem must not fail the source") + require.NotNil(t, src) + t.Cleanup(func() { _ = src.Close() }) + + desc := src.Describe() + assert.Equal(t, tt.wantName, desc.Name) + assert.Equal(t, tt.wantVersion, desc.Version) + + meta, ok := desc.Metadata.(source.SnapMetadata) + require.True(t, ok, "expected snap metadata") + assert.Equal(t, tt.wantSummary, meta.Summary, "fields that did decode must survive") + + // the whole point: the payload is still walkable, so cataloging sees what is inside + r, err := src.FileResolver(source.SquashedScope) + require.NoError(t, err) + locations, err := r.FilesByPath("/payload.txt") + require.NoError(t, err) + assert.Len(t, locations, 1, "contents must remain catalogable regardless of the manifest") + }) + } +} + +func TestSnapSourceClose(t *testing.T) { + newSource := func(closeErr, cleanupErr error) (*snapSource, *int) { + var cleanupRuns int + return &snapSource{ + mutex: &sync.Mutex{}, + squashFileCloser: func() error { + return closeErr + }, + closer: func() error { + cleanupRuns++ + return cleanupErr + }, + }, &cleanupRuns + } + + t.Run("temp directory is removed even when closing the file handle fails", func(t *testing.T) { + s, cleanupRuns := newSource(errors.New("handle is busy"), nil) + + err := s.Close() + + // the whole point: a failure here used to return early and skip the cleanup below, leaving a + // downloaded snap on disk + require.Error(t, err) + assert.Contains(t, err.Error(), "handle is busy") + assert.Equal(t, 1, *cleanupRuns, "temp directory removal was skipped") + }) + + t.Run("every failure is reported, not just the first", func(t *testing.T) { + s, _ := newSource(errors.New("handle is busy"), errors.New("directory is busy")) + + err := s.Close() + + require.Error(t, err) + assert.Contains(t, err.Error(), "handle is busy") + assert.Contains(t, err.Error(), "directory is busy") + }) + + t.Run("is safe to call twice", func(t *testing.T) { + s, cleanupRuns := newSource(nil, nil) + + require.NoError(t, s.Close()) + require.NoError(t, s.Close()) + assert.Equal(t, 1, *cleanupRuns, "cleanup must not run again on a second close") + }) +} + +// writeSquashfs builds a real squashfs image containing /payload.txt and, when manifest is non-empty, +// /meta/snap.yaml. Returns the path to the image. +func writeSquashfs(t *testing.T, manifest string) string { + t.Helper() + + snapPath := filepath.Join(t.TempDir(), "test.snap") + f, err := os.Create(snapPath) + require.NoError(t, err) + defer func() { require.NoError(t, f.Close()) }() + + fs, err := squashfs.Create(diskFile.New(f, false), 0, 0, 4096) + require.NoError(t, err) + + write := func(path, contents string) { + t.Helper() + w, err := fs.OpenFile(path, os.O_CREATE|os.O_RDWR) + require.NoError(t, err) + _, err = w.Write([]byte(contents)) + require.NoError(t, err) + } + + // note: go-diskfs wants paths relative to the image root, without a leading slash + require.NoError(t, fs.Mkdir(".")) + write("payload.txt", "hello") + + if manifest != "" { + require.NoError(t, fs.Mkdir("meta")) + write(strings.TrimPrefix(manifestLocation, "/"), manifest) + } + + require.NoError(t, fs.Finalize(squashfs.FinalizeOptions{})) + return snapPath +} diff --git a/syft/source/snapsource/snap_test.go b/syft/source/snapsource/snap_test.go index 2a0dbbe48..e6b3ac450 100644 --- a/syft/source/snapsource/snap_test.go +++ b/syft/source/snapsource/snap_test.go @@ -264,6 +264,10 @@ func TestNewSnapFileFromRemote(t *testing.T) { _, err = fs.Stat(result.Path) assert.True(t, os.IsNotExist(err)) + + // cleanup owns the whole temp directory, not just the snap file within it + _, err = fs.Stat(filepath.Dir(result.Path)) + assert.True(t, os.IsNotExist(err), "temp directory outlived the cleanup closure") }, }, { @@ -315,6 +319,75 @@ func TestNewSnapFileFromRemote(t *testing.T) { expectError: true, errorMsg: "failed to download snap file", }, + { + name: "download writes partial data then fails", + cfg: Config{ + DigestAlgorithms: []crypto.Hash{crypto.SHA256}, + }, + info: &remoteSnap{ + snapIdentity: snapIdentity{ + Name: "partial-snap", + Channel: "stable", + Architecture: "amd64", + }, + URL: "https://api.snapcraft.io/download/partial_snap.snap", + }, + setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) { + mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/partial_snap.snap", mock.Anything).Run(func(args mock.Arguments) { + // simulate a transfer that writes some bytes before the connection drops + require.NoError(t, createMockSquashfsFile(fs, args.String(0))) + }).Return(fmt.Errorf("connection reset by peer")) + }, + expectError: true, + errorMsg: "failed to download snap file", + }, + { + // isSquashFSFile trusts the .snap extension, so non-squashfs content under that name is accepted + name: "downloaded file has a snap extension but non-snap content", + cfg: Config{ + DigestAlgorithms: []crypto.Hash{crypto.SHA256}, + }, + info: &remoteSnap{ + snapIdentity: snapIdentity{ + Name: "not-a-snap", + Channel: "stable", + Architecture: "amd64", + }, + URL: "https://api.snapcraft.io/download/not_a_snap.snap", + }, + setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) { + mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/not_a_snap.snap", mock.Anything).Run(func(args mock.Arguments) { + require.NoError(t, afero.WriteFile(fs, args.String(0), []byte("not squashfs"), 0644)) + }).Return(nil) + }, + expectError: false, + validate: func(t *testing.T, result *snapFile, fs afero.Fs) { + assert.NotNil(t, result) + assert.Contains(t, result.Path, "not_a_snap.snap") + assert.NotNil(t, result.Cleanup) + }, + }, + { + name: "downloaded file is not a snap", + cfg: Config{ + DigestAlgorithms: []crypto.Hash{crypto.SHA256}, + }, + info: &remoteSnap{ + snapIdentity: snapIdentity{ + Name: "not-a-snap", + Channel: "stable", + Architecture: "amd64", + }, + URL: "https://api.snapcraft.io/download/not_a_snap.tar.gz", + }, + setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) { + mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/not_a_snap.tar.gz", mock.Anything).Run(func(args mock.Arguments) { + require.NoError(t, afero.WriteFile(fs, args.String(0), []byte("not squashfs"), 0644)) + }).Return(nil) + }, + expectError: true, + errorMsg: "not a valid squashfs/snap file", + }, } for _, tt := range tests { @@ -334,8 +407,18 @@ func TestNewSnapFileFromRemote(t *testing.T) { assert.Contains(t, err.Error(), tt.errorMsg) } assert.Nil(t, result) + + // the temp directory and any partially downloaded payload must not be left behind. The + // download destination handed to the getter lives directly under the temp directory. + require.Len(t, mockGetter.Calls, 1) + tempDir := filepath.Dir(mockGetter.Calls[0].Arguments.String(0)) + _, statErr := fs.Stat(tempDir) + assert.True(t, os.IsNotExist(statErr), "temp directory %q was not cleaned up after failure", tempDir) } else { require.NoError(t, err) + if result.Cleanup != nil { + t.Cleanup(func() { _ = result.Cleanup() }) + } if tt.validate != nil { tt.validate(t, result, fs) } From dd639c09b2852fd5716fc7a1b62d1cad08d74909 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Thu, 30 Jul 2026 06:59:36 -0400 Subject: [PATCH 104/168] chore(deps): update tool versions (#5124) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .binny.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.binny.yaml b/.binny.yaml index 92d156f88..da8e9cab2 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -16,7 +16,7 @@ tools: # used to upload test fixture cache - name: oras version: - want: v1.3.2 + want: v1.3.3 method: github-release with: repo: oras-project/oras From ecd8fb77400cb86b44ec5d92ea48a22a5dfc4f9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:27:08 +0000 Subject: [PATCH 105/168] chore(deps): bump docker/login-action from 4.4.0 to 4.5.1 (#5135) Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.1. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.5.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 281e421da..9310a16c1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -89,13 +89,13 @@ jobs: uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Login to Docker Hub - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0 + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 #v4.5.1 with: username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }} password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }} - name: Login to GitHub Container Registry - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0 + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 #v4.5.1 with: registry: ghcr.io username: ${{ github.actor }} From 138e42b428ec58f8e9d424263781f2755bee3591 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:27:32 +0000 Subject: [PATCH 106/168] chore(deps): bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1 (#5134) Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/6599ee8b7a49aef6a770f63d261d214911a7ce02...6fc4b006235f201fdab3722e17240ab420d580e5) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/validate-github-actions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index 0ed9051a9..4df815645 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: "Run zizmor" - uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0 + uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 with: # there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default) advanced-security: true From 4d823eaaa77047bdef0ce5b390b7e67ea330b304 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:27:56 +0000 Subject: [PATCH 107/168] chore(deps): bump github.com/diskfs/go-diskfs from 1.9.3 to 1.9.4 (#5136) Bumps [github.com/diskfs/go-diskfs](https://github.com/diskfs/go-diskfs) from 1.9.3 to 1.9.4. - [Commits](https://github.com/diskfs/go-diskfs/compare/v1.9.3...v1.9.4) --- updated-dependencies: - dependency-name: github.com/diskfs/go-diskfs dependency-version: 1.9.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1c789c699..bde55e541 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/dave/jennifer v1.7.1 github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da - github.com/diskfs/go-diskfs v1.9.3 + github.com/diskfs/go-diskfs v1.9.4 github.com/distribution/reference v0.6.0 github.com/dustin/go-humanize v1.0.1 github.com/elliotchance/phpserialize v1.4.0 diff --git a/go.sum b/go.sum index 41d08cf8e..d79442161 100644 --- a/go.sum +++ b/go.sum @@ -333,8 +333,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= -github.com/diskfs/go-diskfs v1.9.3 h1:cLciNCeZ4QAXVxyPJDr1ZJ9N9CCG3rQlQ/z/Cs/cNDM= -github.com/diskfs/go-diskfs v1.9.3/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc= +github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc= +github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= From 0975d80dc01b7e9a79df81ad72dfe0f00c0d9db4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:28:49 +0000 Subject: [PATCH 108/168] chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.8.2 to 6.8.3 (#5137) Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.8.2 to 6.8.3. - [Release notes](https://github.com/jedib0t/go-pretty/releases) - [Commits](https://github.com/jedib0t/go-pretty/compare/v6.8.2...v6.8.3) --- updated-dependencies: - dependency-name: github.com/jedib0t/go-pretty/v6 dependency-version: 6.8.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bde55e541..3d1c11f26 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/iancoleman/strcase v0.3.0 github.com/invopop/jsonschema v0.14.0 - github.com/jedib0t/go-pretty/v6 v6.8.2 + github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 github.com/klauspost/compress v1.19.0 diff --git a/go.sum b/go.sum index d79442161..80c4fa909 100644 --- a/go.sum +++ b/go.sum @@ -624,8 +624,8 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.8.2 h1:FmKNr1GOyot/zqNQplE8HLhFguJaeHJTCArntnI4uxE= -github.com/jedib0t/go-pretty/v6 v6.8.2/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= From fa6f6d70f0505360568ef77dcf97364102ec5637 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:29:04 +0000 Subject: [PATCH 109/168] chore(deps): bump modernc.org/sqlite from 1.54.0 to 1.55.0 (#5138) Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.54.0 to 1.55.0. - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.54.0...v1.55.0) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.55.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3d1c11f26..b4b096edd 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.48.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.54.0 + modernc.org/sqlite v1.55.0 ) require ( diff --git a/go.sum b/go.sum index 80c4fa909..b3ce5d5c1 100644 --- a/go.sum +++ b/go.sum @@ -1535,8 +1535,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= -modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM= +modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From e00aeb16e448f05a5261935e06ad144d2cee3a24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:32:58 +0000 Subject: [PATCH 110/168] chore(deps): bump github.com/ulikunitz/xz from 0.5.15 to 0.5.16 (#5142) Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.15 to 0.5.16. - [Commits](https://github.com/ulikunitz/xz/compare/v0.5.15...v0.5.16) --- updated-dependencies: - dependency-name: github.com/ulikunitz/xz dependency-version: 0.5.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b4b096edd..f6a7d8736 100644 --- a/go.mod +++ b/go.mod @@ -83,7 +83,7 @@ require ( github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - github.com/ulikunitz/xz v0.5.15 + github.com/ulikunitz/xz v0.5.16 github.com/vbatts/go-mtree v0.7.0 github.com/vifraa/gopom v1.0.0 github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 diff --git a/go.sum b/go.sum index b3ce5d5c1..20c5ebae2 100644 --- a/go.sum +++ b/go.sum @@ -947,8 +947,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0= +github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E= github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY= github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0= From b14123ecc19778e2dcaf59483b128e22a4243c40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:34:20 +0000 Subject: [PATCH 111/168] chore(deps): bump github.com/magiconair/properties (#5141) Bumps [github.com/magiconair/properties](https://github.com/magiconair/properties) from 1.8.10 to 1.18.11. - [Release notes](https://github.com/magiconair/properties/releases) - [Commits](https://github.com/magiconair/properties/compare/v1.8.10...v1.18.11) --- updated-dependencies: - dependency-name: github.com/magiconair/properties dependency-version: 1.18.11 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f6a7d8736..351d35dbc 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 github.com/klauspost/compress v1.19.0 - github.com/magiconair/properties v1.8.10 + github.com/magiconair/properties v1.18.11 github.com/mholt/archives v0.1.5 github.com/moby/sys/mountinfo v0.7.2 github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 diff --git a/go.sum b/go.sum index 20c5ebae2..25ce521b7 100644 --- a/go.sum +++ b/go.sum @@ -670,8 +670,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0= +github.com/magiconair/properties v1.18.11/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= From 80212d87c26054dc04493338a49f0cf1edb915dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:35:46 +0000 Subject: [PATCH 112/168] chore(deps): bump github.com/go-git/go-billy/v5 from 5.9.0 to 5.9.1 (#5139) Bumps [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) from 5.9.0 to 5.9.1. - [Release notes](https://github.com/go-git/go-billy/releases) - [Commits](https://github.com/go-git/go-billy/compare/v5.9.0...v5.9.1) --- updated-dependencies: - dependency-name: github.com/go-git/go-billy/v5 dependency-version: 5.9.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 351d35dbc..9626b54e5 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/facebookincubator/nvdtools v0.1.5 github.com/github/go-spdx/v2 v2.7.0 github.com/gkampitakis/go-snaps v0.5.23 - github.com/go-git/go-billy/v5 v5.9.0 + github.com/go-git/go-billy/v5 v5.9.1 github.com/go-git/go-git/v5 v5.19.1 github.com/go-test/deep v1.1.1 github.com/go-viper/mapstructure/v2 v2.5.0 diff --git a/go.sum b/go.sum index 25ce521b7..234fbf3e5 100644 --- a/go.sum +++ b/go.sum @@ -418,8 +418,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= -github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA= +github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= From ef40eec38d030ca64bfc54a9df9f17fca2cbbb8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:50:21 +0000 Subject: [PATCH 113/168] chore(deps): bump github.com/klauspost/compress from 1.19.0 to 1.19.1 (#5140) Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.0 to 1.19.1. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.19.0...v1.19.1) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.19.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9626b54e5..31eead8c6 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 - github.com/klauspost/compress v1.19.0 + github.com/klauspost/compress v1.19.1 github.com/magiconair/properties v1.18.11 github.com/mholt/archives v0.1.5 github.com/moby/sys/mountinfo v0.7.2 diff --git a/go.sum b/go.sum index 234fbf3e5..7d541412d 100644 --- a/go.sum +++ b/go.sum @@ -645,8 +645,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= -github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= From 7f73d6a6038aeabdc44b4785ca4880cc510ce620 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 3 Aug 2026 13:20:36 -0400 Subject: [PATCH 114/168] chore(deps): update CPE dictionary index (#5148) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index cc31a1c8c..6c01aaf22 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -2333,6 +2333,9 @@ "@ais-ltd/strategyen": [ "cpe:2.3:a:ais-ltd:strategyen:*:*:*:*:*:node.js:*:*" ], + "@amoy/common": [ + "cpe:2.3:a:amoyjs:common:*:*:*:*:*:node.js:*:*" + ], "@angular/core": [ "cpe:2.3:a:angular:angular:*:*:*:*:*:node.js:*:*", "cpe:2.3:a:angularjs:angularjs:*:*:*:*:*:*:*:*" @@ -4252,6 +4255,9 @@ "json8-merge-patch": [ "cpe:2.3:a:json8-merge-patch_project:json8-merge-patch:*:*:*:*:*:node.js:*:*" ], + "jsonpath": [ + "cpe:2.3:a:dchester:jsonpath:*:*:*:*:*:node.js:*:*" + ], "jsonpointer": [ "cpe:2.3:a:janl:jsonpointer:*:*:*:*:*:node.js:*:*" ], @@ -6858,6 +6864,9 @@ "pydantic-ai": [ "cpe:2.3:a:pydantic:pydantic_ai:*:*:*:*:*:python:*:*" ], + "pydantic-settings": [ + "cpe:2.3:a:pydantic:pydantic-settings:*:*:*:*:*:*:*:*" + ], "pydash": [ "cpe:2.3:a:derrickgilland:pydash:*:*:*:*:*:python:*:*" ], @@ -7208,6 +7217,9 @@ "encoded_id-rails": [ "cpe:2.3:a:diaconou:encodedid\\:\\:rails:*:*:*:*:*:ruby:*:*" ], + "excon": [ + "cpe:2.3:a:excon_project:excon:*:*:*:*:*:ruby:*:*" + ], "faye": [ "cpe:2.3:a:faye_project:faye:*:*:*:*:*:ruby:*:*" ], @@ -7615,6 +7627,9 @@ "ammonia": [ "cpe:2.3:a:ammonia_project:ammonia:*:*:*:*:*:rust:*:*" ], + "anchor-lang": [ + "cpe:2.3:a:anchor-lang:anchor:*:*:*:*:*:rust:*:*" + ], "anymap": [ "cpe:2.3:a:anymap_project:anymap:*:*:*:*:*:rust:*:*" ], @@ -7984,6 +7999,9 @@ "one2html": [ "cpe:2.3:a:msiemens:one2html:*:*:*:*:*:rust:*:*" ], + "openssl": [ + "cpe:2.3:a:sfackler:openssl:*:*:*:*:*:rust:*:*" + ], "outer_cgi": [ "cpe:2.3:a:outer_cgi_project:outer_cgi:*:*:*:*:*:rust:*:*" ], @@ -8883,6 +8901,9 @@ "album-and-image-gallery-plus-lightbox": [ "cpe:2.3:a:essentialplugin:album_and_image_gallery_plus_lightbox:*:*:*:*:free:wordpress:*:*" ], + "alemha-watermark": [ + "cpe:2.3:a:mnbaa:alemha_watermark:*:*:*:*:*:wordpress:*:*" + ], "alfred-click-collect": [ "cpe:2.3:a:alfred24_click_\\\u0026_collect_project:alfred24_click_\\\u0026_collect:*:*:*:*:*:wordpress:*:*" ], @@ -9510,6 +9531,9 @@ "barcode-scanner-lite-pos-to-manage-products-inventory-and-orders": [ "cpe:2.3:a:ukrsolution:barcode_scanner_and_inventory_manager:*:*:*:*:*:wordpress:*:*" ], + "base64-encoderdecoder": [ + "cpe:2.3:a:mranderson:base64_encoder\\/decoder:*:*:*:*:*:wordpress:*:*" + ], "basic-interactive-world-map": [ "cpe:2.3:a:wpmapplugins:basic_interactive_world_map:*:*:*:*:*:wordpress:*:*" ], @@ -9964,6 +9988,9 @@ "bsuite": [ "cpe:2.3:a:bsuite_project:bsuite:*:*:*:*:*:wordpress:*:*" ], + "bu-section-editing": [ + "cpe:2.3:a:bu:bu_section_editing:*:*:*:*:*:wordpress:*:*" + ], "bubble-menu": [ "cpe:2.3:a:wow-company:bubble_menu:*:*:*:*:free:wordpress:*:*" ], @@ -11688,6 +11715,9 @@ "donation-thermometer": [ "cpe:2.3:a:donation_thermometer_project:donation_thermometer:*:*:*:*:*:wordpress:*:*" ], + "donations-block": [ + "cpe:2.3:a:bharatkambariya:donation_block_for_paypal:*:*:*:*:*:wordpress:*:*" + ], "donations-for-woocommerce": [ "cpe:2.3:a:wpzone:potent_donations_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], @@ -11775,6 +11805,9 @@ "dtabs": [ "cpe:2.3:a:dtabs_project:dtabs:*:*:*:*:*:wordpress:*:*" ], + "dts-simple-share": [ + "cpe:2.3:a:missionmike:simple_share:*:*:*:*:*:wordpress:*:*" + ], "duitku-social-payment-gateway": [ "cpe:2.3:a:duitku:duitku_payment_gateway:*:*:*:*:*:wordpress:*:*" ], @@ -12763,6 +12796,9 @@ "five-minute-webshop": [ "cpe:2.3:a:five_minute_webshop_project:five_minute_webshop:*:*:*:*:*:wordpress:*:*" ], + "fl3r-accessibility-suite": [ + "cpe:2.3:a:fl3r:fl3r_accessibility_suite:*:*:*:*:*:wordpress:*:*" + ], "fl3r-feelbox": [ "cpe:2.3:a:armandofiore:fl3r_feelbox:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:fl3r-feelbox_project:fl3r-feelbox:*:*:*:*:*:wordpress:*:*" @@ -13280,6 +13316,9 @@ "gf-block-ips": [ "cpe:2.3:a:brightplugins:block_ips_for_gravity_forms:*:*:*:*:*:wordpress:*:*" ], + "gf-constant-contact": [ + "cpe:2.3:a:rocketgenius:constant_contact:*:*:*:*:*:wordpress:*:*" + ], "gf-custom-style": [ "cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*" ], @@ -15469,6 +15508,9 @@ "mihdan-index-now": [ "cpe:2.3:a:kobzarev:index_now:*:*:*:*:*:wordpress:*:*" ], + "mini-course-generator": [ + "cpe:2.3:a:minicoursegenerator:mini_course_generator:*:*:*:*:*:wordpress:*:*" + ], "mini-mail-dashboard-widget": [ "cpe:2.3:a:mini_mail_dashboard_widget_project:mini_mail_dashboard_widget:*:*:*:*:*:wordpress:*:*" ], @@ -15502,6 +15544,9 @@ "ml-slider": [ "cpe:2.3:a:metaslider:slider\\,_gallery\\,_and_carousel:*:*:*:*:*:wordpress:*:*" ], + "mm-breaking-news": [ + "cpe:2.3:a:mm-breaking_news_project:mm-breaking_news:*:*:*:*:*:wordpress:*:*" + ], "mmm-file-list": [ "cpe:2.3:a:mediamanifesto:mmm_simple_file_list:*:*:*:*:*:wordpress:*:*" ], @@ -15588,6 +15633,9 @@ "mortgage-calculators-wp": [ "cpe:2.3:a:lenderd:mortgage_calculators_wp:*:*:*:*:*:wordpress:*:*" ], + "mortgage-loan-calculator": [ + "cpe:2.3:a:mlcalc:mortgage_loan_calculator:*:*:*:*:*:wordpress:*:*" + ], "most-and-least-read-posts-widget": [ "cpe:2.3:a:whiletrue:most_and_least_read_posts_widget:*:*:*:*:*:wordpress:*:*" ], @@ -16081,6 +16129,9 @@ "official-sendle-shipping-method": [ "cpe:2.3:a:joovii:sendle_shipping:*:*:*:*:*:wordpress:*:*" ], + "offload-videos-bunny-netaws-s3": [ + "cpe:2.3:a:webgarh:offload_videos:*:*:*:*:*:wordpress:*:*" + ], "og-tags": [ "cpe:2.3:a:og_tags_project:og_tags:*:*:*:*:*:wordpress:*:*" ], @@ -17247,7 +17298,10 @@ "cpe:2.3:a:wepupil:quiz_expert_-_easy_quiz_maker\\,_exam_and_test_manager:*:*:*:*:*:wordpress:*:*" ], "quiz-maker": [ - "cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:agency:wordpress:*:*", + "cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:business:wordpress:*:*", + "cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:developer:wordpress:*:*" ], "quiz-master-next": [ "cpe:2.3:a:expresstech:quiz_and_survey_master:*:*:*:*:*:wordpress:*:*", @@ -17825,6 +17879,9 @@ "schema-app-structured-data-for-schemaorg": [ "cpe:2.3:a:schemaapp:schema_app_structured_data:*:*:*:*:*:wordpress:*:*" ], + "school-management": [ + "cpe:2.3:a:dasinfomedia:school_management_system:*:*:*:*:*:wordpress:*:*" + ], "school-management-system": [ "cpe:2.3:a:weblizar:school_management_-_education_\\\u0026_learning_management:*:*:*:*:*:wordpress:*:*" ], @@ -18209,6 +18266,9 @@ "simple-author-box": [ "cpe:2.3:a:webfactoryltd:simple_author_box:*:*:*:*:*:wordpress:*:*" ], + "simple-backup": [ + "cpe:2.3:a:mywebsiteadvisor:simple_backup:*:*:*:*:*:wordpress:*:*" + ], "simple-banner": [ "cpe:2.3:a:simple_banner_project:simple_banner:*:*:*:*:*:wordpress:*:*" ], @@ -18991,6 +19051,9 @@ "srbtranslatin": [ "cpe:2.3:a:srbtranslatin_project:srbtranslatin:*:*:*:*:*:wordpress:*:*" ], + "ssv-events": [ + "cpe:2.3:a:moridrin:ssv_events:*:*:*:*:*:wordpress:*:*" + ], "st-daily-tip": [ "cpe:2.3:a:sanskruti:st-daily-tip:*:*:*:*:*:wordpress:*:*" ], @@ -19105,6 +19168,9 @@ "strategery-migrations": [ "cpe:2.3:a:strategery_migrations_project:strategery_migrations:*:*:*:*:*:wordpress:*:*" ], + "stratum": [ + "cpe:2.3:a:motopress:stratum:*:*:*:*:*:wordpress:*:*" + ], "stray-quotes": [ "cpe:2.3:a:unalignedcode:stray_random_quotes:*:*:*:*:*:wordpress:*:*" ], @@ -19135,6 +19201,9 @@ "subaccounts-for-woocommerce": [ "cpe:2.3:a:mediaticus:subaccounts_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], + "sublanguage": [ + "cpe:2.3:a:maximeschoeni:sublanguage:*:*:*:*:*:wordpress:*:*" + ], "subscribe-sidebar": [ "cpe:2.3:a:blubrry:subscribe_sidebar:*:*:*:*:*:wordpress:*:*" ], @@ -19571,12 +19640,18 @@ "timetics": [ "cpe:2.3:a:arraytics:wp_timetics:*:*:*:*:*:wordpress:*:*" ], + "timezonecalculator": [ + "cpe:2.3:a:bernhard-riedl:timezonecalculator:*:*:*:*:*:wordpress:*:*" + ], "timthumb-vulnerability-scanner": [ "cpe:2.3:a:peterbutler:timthumb_vulnerability_scanner:*:*:*:*:*:wordpress:*:*" ], "tinychat-roomspy": [ "cpe:2.3:a:tinychat:room_spy:*:*:*:*:*:wordpress:*:*" ], + "tinymce-and-tinymce-advanced-professsional-formats-and-styles": [ + "cpe:2.3:a:blackbam:tinymce_and_tinymce_advanced_professsional_formats_and_styles:*:*:*:*:*:wordpress:*:*" + ], "tinymce-custom-styles": [ "cpe:2.3:a:tinymce_custom_styles_project:tinymce_custom_styles:*:*:*:*:*:wordpress:*:*" ], @@ -19645,6 +19720,9 @@ "tournamatch": [ "cpe:2.3:a:tournamatch:tournamatch:*:*:*:*:*:wordpress:*:*" ], + "tournament-bracket-generator": [ + "cpe:2.3:a:blakelong:tournament_bracket_generator:*:*:*:*:*:wordpress:*:*" + ], "tp-education": [ "cpe:2.3:a:themepalace:tp_education:*:*:*:*:*:wordpress:*:*" ], @@ -20412,6 +20490,9 @@ "weaverx-theme-support": [ "cpe:2.3:a:weavertheme:weaver_xtreme_theme_support:*:*:*:*:*:wordpress:*:*" ], + "web-application-firewall": [ + "cpe:2.3:a:miniorange:web_application_firewall:*:*:*:*:*:wordpress:*:*" + ], "web-directory-free": [ "cpe:2.3:a:salephpscripts:web_directory_free:*:*:*:*:*:wordpress:*:*" ], @@ -20941,6 +21022,9 @@ "wooemailreport": [ "cpe:2.3:a:eggemplo:woocommerce_email_report:*:*:*:*:*:wordpress:*:*" ], + "wooexim": [ + "cpe:2.3:a:bulktheme:wooexim:*:*:*:*:*:wordpress:*:*" + ], "wooframework-tweaks": [ "cpe:2.3:a:woocommerce:wooframework_tweaks:*:*:*:*:*:wordpress:*:*" ], @@ -21754,6 +21838,9 @@ "wp-jobs": [ "cpe:2.3:a:intensewp:wp_jobs:*:*:*:*:*:wordpress:*:*" ], + "wp-jobs2careers": [ + "cpe:2.3:a:skipthedrive:wp_talroo:*:*:*:*:*:wordpress:*:*" + ], "wp-jquery-lightbox": [ "cpe:2.3:a:lightpress:lightbox:*:*:*:*:*:wordpress:*:*" ], @@ -22730,7 +22817,8 @@ "cpe:2.3:a:maennchen1:wpshopgermany_-_protected_shops:*:*:*:*:*:wordpress:*:*" ], "wpshopify": [ - "cpe:2.3:a:shopwp_project:shopwp:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:shopwp_project:shopwp:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:wpshop:shopwp:*:*:*:*:*:wordpress:*:*" ], "wpsimpletools-log-viewer": [ "cpe:2.3:a:wpsimpletools:basic_log_viewer:*:*:*:*:*:wordpress:*:*" @@ -23504,6 +23592,9 @@ "tijaji": [ "cpe:2.3:a:tijaji:tijaji:*:*:*:*:*:wordpress:*:*" ], + "total": [ + "cpe:2.3:a:hashthemes:total:*:*:*:*:*:wordpress:*:*" + ], "transcend": [ "cpe:2.3:a:cpothemes:transcend:*:*:*:*:*:wordpress:*:*" ], From 35bf33bcf05537cfc17382910037ff3efc7a3cf7 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 4 Aug 2026 10:46:35 -0400 Subject: [PATCH 115/168] fix(make): explicit integration test timeout + single race switch (#5151) with a cold fixture cache the integration suite builds and saves 18 docker images across 36 sequential tests, which walks past `go test`'s default 10m timeout and takes the fixture cache rebuild down with it. that suite now runs `go test` directly with `-timeout=30m` (gotest.Tasks() has no timeout option), plus `-count=1` since the built fixtures are the side effect we're actually after and a test cache hit would skip producing them. also adds `RACE` as one switch for the race detector across every suite: - `RACE=false make test` drops `-race` from unit + integration and skips the race smoke, worth doing on a cache rebuild where the wall clock is all docker builds anyway - `RACE=true` forces it on locally - unset behaves as before: on in CI, off locally and on windows Signed-off-by: Alex Goodman --- .make/main.go | 61 +++++++++++++++++++++++++++++++++++++++------- .make/main_test.go | 45 ++++++++++++++++++++++++++++++++++ Taskfile.yaml | 1 + 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 .make/main_test.go diff --git a/.make/main.go b/.make/main.go index 90fcffea7..f0f08700d 100644 --- a/.make/main.go +++ b/.make/main.go @@ -3,8 +3,10 @@ package main import ( "path/filepath" "runtime" + "strconv" . "github.com/anchore/go-make" + "github.com/anchore/go-make/config" "github.com/anchore/go-make/file" "github.com/anchore/go-make/lang" "github.com/anchore/go-make/run" @@ -27,22 +29,43 @@ func main() { gotest.Name("unit"), gotest.ExcludeGlob("**/test/**"), gotest.CoverageThreshold(62), + race(), ), - // integration tests: native go-make Task. The race-detector smoke against a - // real image stays bundled here (RunsOn integration) so `make integration` - // behaves like the Taskfile version did. - gotest.Tasks( - gotest.Name("integration"), - gotest.IncludeGlob("./cmd/syft/internal/test/integration/..."), - gotest.Verbose(), - gotest.NoCoverage(), - ), + // integration tests: run `go test` directly instead of via gotest.Tasks(), which + // has no way to set a timeout. The suite is a single package of ~36 sequential + // tests over 18 docker fixture images, so with a cold fixture cache (every image + // built + saved inline, under -race) it runs well past `go test`'s default 10m + // timeout -- which is exactly the case when regenerating the fixture cache from + // scratch. -count=1 keeps the go test cache from short-circuiting a run whose + // side effect (the built fixtures) is the thing we're after. + // + // The race-detector smoke against a real image stays bundled here (RunsOn + // integration) so `make integration` behaves like the Taskfile version did. + Task{ + Name: "integration", + Description: "run integration tests", + RunsOn: lang.List("test"), + Run: func() { + raceFlag := "" + if raceEnabled() { + raceFlag = " -race" + } + Run( + "go test -count=1 -timeout=30m -v"+raceFlag+" ./cmd/syft/internal/test/integration/...", + run.Env("GODEBUG", "dontfreezetheworld=1"), + ) + }, + }, Task{ Name: "integration:race-smoke", Description: "exercise the CLI with the race detector", RunsOn: lang.List("integration"), Run: func() { + if !raceEnabled() { + Log("race detector disabled (RACE=false); skipping race smoke") + return + } Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2") }, }, @@ -132,6 +155,26 @@ func main() { ) } +// raceEnabled is the single switch for the race detector across every test suite. +// Unset it and we keep go-make's behavior (on in CI, off locally and on windows); +// set RACE=false to turn it off everywhere -- worth doing when rebuilding the test +// fixture cache from scratch, where every suite is dominated by building docker +// fixtures and the race detector only adds wall clock. RACE=true forces it on. +func raceEnabled() bool { + if enabled, err := strconv.ParseBool(config.Env("RACE", "")); err == nil { + return enabled + } + return config.CI && !config.Windows +} + +// race applies raceEnabled() to a gotest suite. gotest exposes no functional option +// for the race detector, but gotest.Config.Race is exported. +func race() gotest.Option { + return func(c *gotest.Config) { + c.Race = raceEnabled() + } +} + // snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile: // /snapshot/-build__/syft, where arch maps amd64->amd64_v1 // and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming. diff --git a/.make/main_test.go b/.make/main_test.go new file mode 100644 index 000000000..8be12632d --- /dev/null +++ b/.make/main_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "testing" + + "github.com/anchore/go-make/config" + "github.com/anchore/go-make/tasks/gotest" +) + +func TestRaceEnabled(t *testing.T) { + tests := []struct { + name string + env string // "" means RACE is unset + ci bool + want bool + }{ + {name: "default in CI", ci: true, want: true}, + {name: "default locally", ci: false, want: false}, + {name: "explicitly off in CI", env: "false", ci: true, want: false}, + {name: "explicitly on locally", env: "true", ci: false, want: true}, + {name: "unparseable falls back to default", env: "yes-please", ci: true, want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != "" { + t.Setenv("RACE", tt.env) + } + orig := config.CI + config.CI = tt.ci + t.Cleanup(func() { config.CI = orig }) + + if got := raceEnabled(); got != tt.want { + t.Errorf("raceEnabled() = %v, want %v", got, tt.want) + } + + // the gotest suites get the same answer through the functional option + var cfg gotest.Config + race()(&cfg) + if cfg.Race != tt.want { + t.Errorf("race() set Race = %v, want %v", cfg.Race, tt.want) + } + }) + } +} diff --git a/Taskfile.yaml b/Taskfile.yaml index 7e671c38e..a0ced95e5 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -125,6 +125,7 @@ tasks: desc: Run tests for pipeline utils cmds: - cmd: .github/scripts/labeler_test.py + - cmd: go test -C .make ./... snapshot-smoke-test: desc: Run a smoke test on the snapshot builds + docker images From 68da404bd7c8ac00630f322fa779fbf4b92f6deb Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 5 Aug 2026 10:00:17 -0400 Subject: [PATCH 116/168] fix(make): don't let ambient RACE leak into raceEnabled test (#5152) The two "RACE is unset" cases only skipped the t.Setenv call, so they inherited whatever RACE was in the environment. Running `make test` with RACE=false exported job-wide flipped the CI-default case and failed. Signed-off-by: Alex Goodman Signed-off-by: Alex Goodman --- .make/main_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.make/main_test.go b/.make/main_test.go index 8be12632d..50c6769d6 100644 --- a/.make/main_test.go +++ b/.make/main_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "testing" "github.com/anchore/go-make/config" @@ -23,8 +24,11 @@ func TestRaceEnabled(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.env != "" { - t.Setenv("RACE", tt.env) + // always go through t.Setenv (it registers the restore) so an ambient + // RACE from the caller's environment can't leak into the unset cases + t.Setenv("RACE", tt.env) + if tt.env == "" { + os.Unsetenv("RACE") } orig := config.CI config.CI = tt.ci From da745b13e888674845de60b067a37af361ceb5ee Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 7 Aug 2026 08:44:42 -0400 Subject: [PATCH 117/168] feat(golang): add extended-stdlib scope and module patterns for symbol capture (#5154) * feat(golang): add extended-stdlib scope and include patterns for symbol capture `golang.capture-symbols` decides how much symbol data lands in the SBOM for grype's reachability analysis. It's `none`, `stdlib`, or `all` today, and the useful middle is missing: `stdlib` stops at the standard library, `all` multiplies SBOM size. A new `extended-stdlib` configurable covers stdlib plus everything under `golang.org/x/`: ```yaml golang: capture-symbols: extended-stdlib ``` Also, a new `capture-symbols-include` configurable for modules that are noisy in your binaries but not everyone's. It's unioned with whatever the scope selects, so it only ever widens: ```yaml golang: capture-symbols: extended-stdlib capture-symbols-include: - github.com/klauspost/** ``` Patterns are standard doublestar globs, which matters because module paths carry `/v2`-style suffixes: ```yaml golang: capture-symbols-include: - github.com/klauspost/* # compress, but not compress/v2 - github.com/klauspost/** # both - k8s.io/client-go # exact match only ``` Ordering is `none` < `stdlib` < `extended-stdlib` < `all`. The existing three values and the `none` default are unchanged, and the include list is inert under `none`. Presets compile into glob lists internally, so a single matcher answers "does this module get symbols" instead of a preset branch sitting next to a separate glob branch. An unrecognized `capture-symbols` value still falls back to `none`, but warns now instead of doing it silently. A malformed include pattern warns and gets skipped. One thing worth a look beyond the feature: the `Symbols` field description in the JSON schema was wrong after this (it claimed only `all` and `stdlib` populate anything), and that description lives in the already-published `16.1.10`. Rather than bump a version for a sentence, `16.1.10` is amended in place and `schema/json/README.md` grows an explicit exception for description-only changes: descriptions only, no shape change of any kind, `$id` unchanged. Anything else still needs a bump. Happy to split that into its own PR if you'd rather review the policy separately. Signed-off-by: Alex Goodman * refactor(golang): rename capture-symbols-include to capture-symbols-modules The key's entries are go module paths, and `-include` sitting next to `capture-symbols` reads as plausibly taking symbol or package names instead. Those spellings parse and match nothing, which is quieter than the confusion `-include` was picked to avoid, so the name now says what the list holds. `golang.CatalogerConfig.CaptureSymbolsModules` and `WithCaptureSymbolsModules` rename with it. Nothing behavioral changes; the key is new in this PR so there is no compatibility surface. Signed-off-by: Alex Goodman * feat(golang): match capture-symbols-modules across major version suffixes `github.com/anchore/*` covered `github.com/anchore/syft` and silently stopped covering it the day it became `github.com/anchore/syft/v2`. The config keeps parsing, nothing warns, and symbols quietly go missing from the SBOM. Exact paths had the same hole: `github.com/klauspost/compress` did not cover `compress/v2` either, so no spelling short of `**` survived a major bump. A major version suffix is part of a module's path but not part of its identity, so patterns are now matched against the module path both with and without it, using `module.SplitPathVersion` from `golang.org/x/mod` (already a direct dep, already used in this package for `PseudoVersion`). ```yaml golang: capture-symbols-modules: - github.com/klauspost/* # compress and compress/v2 - github.com/klauspost/compress # same module at every major version - github.com/klauspost/compress/v2 # v2 alone ``` Only a trailing suffix is a version, which is Go's own rule. In `github.com/anchore/syft/v2/thing` the `v2` is an ordinary path element naming a major subdirectory a nested module lives in, so it stays literal and `github.com/anchore/**/thing` is how you reach it. `/v0` and `/v1` are not valid suffixes and are left alone. Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- cmd/syft/internal/options/catalog.go | 3 +- cmd/syft/internal/options/golang.go | 34 ++- cmd/syft/internal/options/golang_test.go | 48 ++- internal/capabilities/appconfig.yaml | 4 +- schema/json/README.md | 21 ++ schema/json/schema-16.1.10.json | 2 +- schema/json/schema-latest.json | 2 +- syft/cataloging/symbols.go | 6 + syft/cataloging/symbols_test.go | 5 + syft/pkg/cataloger/golang/capabilities.yaml | 5 +- syft/pkg/cataloger/golang/config.go | 17 +- syft/pkg/cataloger/golang/parse_go_binary.go | 16 +- .../cataloger/golang/parse_go_binary_test.go | 114 ++++++- syft/pkg/cataloger/golang/symbol_selector.go | 113 +++++++ .../cataloger/golang/symbol_selector_test.go | 280 ++++++++++++++++++ syft/pkg/golang.go | 6 +- 16 files changed, 648 insertions(+), 28 deletions(-) create mode 100644 syft/pkg/cataloger/golang/symbol_selector.go create mode 100644 syft/pkg/cataloger/golang/symbol_selector_test.go diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 0d7e94b50..3ff68ecbd 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -199,7 +199,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags), ). WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)). - WithCaptureSymbols(cfg.Golang.CaptureSymbols), + WithCaptureSymbols(cfg.Golang.CaptureSymbols). + WithCaptureSymbolsModules(cfg.Golang.CaptureSymbolsModules), JavaScript: javascript.DefaultCatalogerConfig(). WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)). WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)). diff --git a/cmd/syft/internal/options/golang.go b/cmd/syft/internal/options/golang.go index c811a34c3..e8147a06d 100644 --- a/cmd/syft/internal/options/golang.go +++ b/cmd/syft/internal/options/golang.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/anchore/clio" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/pkg/cataloger/golang" ) @@ -19,6 +20,7 @@ type golangConfig struct { MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"` UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"` CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"` + CaptureSymbolsModules []string `json:"capture-symbols-modules" yaml:"capture-symbols-modules" mapstructure:"capture-symbols-modules"` } var _ interface { @@ -42,8 +44,16 @@ if unset this defaults to $GONOPROXY`) always show (devel) as the version. Use these options to control heuristics to guess a more accurate version from the binary.`) descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`) + // note: descriptions must be static string literals; the app config discovery that generates the + // capability docs reads them straight out of the AST descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are: -"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`) +"none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every +module under golang.org/x/), and "all" (all module packages plus stdlib)`) + descriptions.Add(&o.CaptureSymbolsModules, `glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols +captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. +a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; +spelling a suffix out in the pattern selects only that major version. +this can only widen the selection, never narrow it, and is inert when capture-symbols is none`) descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`) descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version (e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`) @@ -51,7 +61,24 @@ a more accurate version from the binary.`) } func (o *golangConfig) PostLoad() error { + raw := strings.TrimSpace(string(o.CaptureSymbols)) o.CaptureSymbols = o.CaptureSymbols.Parse() + + // an unrecognized value still resolves to "none", but say so rather than silently capturing nothing. + // stay quiet for unset and an explicit "none", which is the default and would otherwise warn on nearly + // every scan. + if o.CaptureSymbols == cataloging.SymbolScopeNone && raw != "" && !strings.EqualFold(raw, string(cataloging.SymbolScopeNone)) { + log.Warnf("unknown golang.capture-symbols value %q, defaulting to %q (valid values: none, stdlib, extended-stdlib, all)", raw, cataloging.SymbolScopeNone) + } + + // trim only, deliberately not Flatten: viper already splits a comma-separated scalar (env var or a bare + // yaml string) into a slice before this point, and there is no CLI flag feeding this key. The one thing + // Flatten would add is splitting commas *inside* a list entry, which silently breaks doublestar brace + // alternation like github.com/{foo,bar}/**. Viper's split does not trim, so that part is still needed. + for i, pattern := range o.CaptureSymbolsModules { + o.CaptureSymbolsModules[i] = strings.TrimSpace(pattern) + } + return nil } @@ -76,7 +103,8 @@ func defaultGolangConfig() golangConfig { FromContents: def.MainModuleVersion.FromContents, FromBuildSettings: def.MainModuleVersion.FromBuildSettings, }, - UsePackagesLib: nil, // this defaults to true, which is the API default - CaptureSymbols: def.CaptureSymbols, + UsePackagesLib: nil, // this defaults to true, which is the API default + CaptureSymbols: def.CaptureSymbols, + CaptureSymbolsModules: def.CaptureSymbolsModules, } } diff --git a/cmd/syft/internal/options/golang_test.go b/cmd/syft/internal/options/golang_test.go index 643349904..b8c1f1370 100644 --- a/cmd/syft/internal/options/golang_test.go +++ b/cmd/syft/internal/options/golang_test.go @@ -10,10 +10,11 @@ import ( func Test_golangConfig_PostLoad(t *testing.T) { tests := []struct { - name string - cfg golangConfig - expected cataloging.SymbolScope - wantErr assert.ErrorAssertionFunc + name string + cfg golangConfig + expected cataloging.SymbolScope + expectedModules []string + wantErr assert.ErrorAssertionFunc }{ { name: "normalize all", @@ -25,6 +26,32 @@ func Test_golangConfig_PostLoad(t *testing.T) { cfg: golangConfig{CaptureSymbols: "stdlib"}, expected: cataloging.SymbolScopeStdlib, }, + { + name: "normalize extended-stdlib", + cfg: golangConfig{CaptureSymbols: " Extended-Stdlib "}, + expected: cataloging.SymbolScopeExtendedStdlib, + }, + { + name: "module patterns keep embedded commas", + cfg: golangConfig{ + CaptureSymbols: "stdlib", + // brace alternation contains a comma; splitting on it would corrupt the pattern + CaptureSymbolsModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"}, + }, + expected: cataloging.SymbolScopeStdlib, + expectedModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"}, + }, + { + // viper splits a comma-separated scalar (env var or bare yaml string) but does not trim, + // so a leading space would otherwise survive into a pattern that silently matches nothing + name: "module patterns are trimmed", + cfg: golangConfig{ + CaptureSymbols: "stdlib", + CaptureSymbolsModules: []string{"golang.org/x/**", " github.com/foo/** "}, + }, + expected: cataloging.SymbolScopeStdlib, + expectedModules: []string{"golang.org/x/**", "github.com/foo/**"}, + }, { name: "empty defaults to none", cfg: golangConfig{CaptureSymbols: ""}, @@ -32,7 +59,7 @@ func Test_golangConfig_PostLoad(t *testing.T) { }, { name: "invalid value defaults to none", - cfg: golangConfig{CaptureSymbols: "bogus"}, + cfg: golangConfig{CaptureSymbols: "stdlbi"}, expected: cataloging.SymbolScopeNone, }, { @@ -40,6 +67,16 @@ func Test_golangConfig_PostLoad(t *testing.T) { cfg: golangConfig{CaptureSymbols: "true"}, expected: cataloging.SymbolScopeNone, }, + { + name: "explicit none resolves to none", + cfg: golangConfig{CaptureSymbols: "none"}, + expected: cataloging.SymbolScopeNone, + }, + { + name: "explicit none is not case sensitive", + cfg: golangConfig{CaptureSymbols: " NONE "}, + expected: cataloging.SymbolScopeNone, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -52,6 +89,7 @@ func Test_golangConfig_PostLoad(t *testing.T) { return } assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols) + assert.Equal(t, tt.expectedModules, tt.cfg.CaptureSymbolsModules) }) } } diff --git a/internal/capabilities/appconfig.yaml b/internal/capabilities/appconfig.yaml index 295df39df..e15fdde0f 100644 --- a/internal/capabilities/appconfig.yaml +++ b/internal/capabilities/appconfig.yaml @@ -15,7 +15,9 @@ application: # AUTO-GENERATED - application-level config keys - key: dotnet.relax-dll-claims-when-bundling-detected description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack) - key: golang.capture-symbols - description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)' + description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib)' + - key: golang.capture-symbols-modules + description: glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; spelling a suffix out in the pattern selects only that major version. this can only widen the selection, never narrow it, and is inert when capture-symbols is none - key: golang.local-mod-cache-dir description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod - key: golang.local-vendor-dir diff --git a/schema/json/README.md b/schema/json/README.md index 52fb639f5..37fd50b0c 100644 --- a/schema/json/README.md +++ b/schema/json/README.md @@ -35,3 +35,24 @@ Create the new schema by running `make generate-json-schema` from the root of th - If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section) ***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory. + +### Exception: `description`-only corrections + +A published schema may be amended in place for one narrow case: the change touches only `description` text and leaves the data shape identical. Descriptions are documentation carried alongside the schema rather than constraints a validator evaluates, so correcting one cannot invalidate a document that already validated against that version. Minting a new version instead would leave the old one permanently describing the tool incorrectly, and spend a version number on no semantic change. + +This applies when **every** one of the following holds: + +- the only differences are `description` values +- no field, type, enum, `required` entry, or `$ref` is added, removed, or altered +- the `$id` version is unchanged + +Anything else, including adding a field that happens to be optional, is a schema change and needs a version bump per the "Versioning" section above. + +The generator blocks an in-place edit by design, since it refuses to overwrite a file that differs from what it would produce. To amend one, delete the schema file and regenerate so it is rewritten from the current Go doc comments: + +```bash +rm schema/json/schema-$VERSION.json +make generate-json-schema +``` + +The result is byte-identical to what the generator produces, so do not hand-edit the JSON. Re-running `make generate-json-schema` afterwards should report `No change to the existing schema!`, and `make check-json-schema-drift` should pass. Confirm with `git diff` that the only changes are the intended description lines. diff --git a/schema/json/schema-16.1.10.json b/schema/json/schema-16.1.10.json index 87756c843..37a658238 100644 --- a/schema/json/schema-16.1.10.json +++ b/schema/json/schema-16.1.10.json @@ -1662,7 +1662,7 @@ "type": "array" }, "type": "object", - "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package." + "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match." } }, "type": "object", diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 87756c843..37a658238 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1662,7 +1662,7 @@ "type": "array" }, "type": "object", - "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package." + "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match." } }, "type": "object", diff --git a/syft/cataloging/symbols.go b/syft/cataloging/symbols.go index 4e892fcee..2b7907eda 100644 --- a/syft/cataloging/symbols.go +++ b/syft/cataloging/symbols.go @@ -12,6 +12,10 @@ const ( // SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols. SymbolScopeStdlib SymbolScope = "stdlib" + // SymbolScopeExtendedStdlib captures symbols for the synthetic "stdlib" package as well as every module + // under golang.org/x/ (the extended standard library). + SymbolScopeExtendedStdlib SymbolScope = "extended-stdlib" + // SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package. SymbolScopeAll SymbolScope = "all" ) @@ -21,6 +25,8 @@ func (s SymbolScope) Parse() SymbolScope { switch strings.ToLower(strings.TrimSpace(string(s))) { case string(SymbolScopeAll): return SymbolScopeAll + case string(SymbolScopeExtendedStdlib): + return SymbolScopeExtendedStdlib case string(SymbolScopeStdlib): return SymbolScopeStdlib } diff --git a/syft/cataloging/symbols_test.go b/syft/cataloging/symbols_test.go index 307119a15..8d8d12717 100644 --- a/syft/cataloging/symbols_test.go +++ b/syft/cataloging/symbols_test.go @@ -16,6 +16,11 @@ func Test_SymbolScope_Parse(t *testing.T) { {" all ", SymbolScopeAll}, {"stdlib", SymbolScopeStdlib}, {"Stdlib", SymbolScopeStdlib}, + {"extended-stdlib", SymbolScopeExtendedStdlib}, + {"Extended-Stdlib", SymbolScopeExtendedStdlib}, + {"EXTENDED-STDLIB", SymbolScopeExtendedStdlib}, + {" extended-stdlib ", SymbolScopeExtendedStdlib}, + {"extended_stdlib", SymbolScopeNone}, {"none", SymbolScopeNone}, {"", SymbolScopeNone}, {"true", SymbolScopeNone}, diff --git a/syft/pkg/cataloger/golang/capabilities.yaml b/syft/pkg/cataloger/golang/capabilities.yaml index 01e24b146..c9c314b19 100644 --- a/syft/pkg/cataloger/golang/capabilities.yaml +++ b/syft/pkg/cataloger/golang/capabilities.yaml @@ -25,8 +25,11 @@ configs: # AUTO-GENERATED - config structs and their fields description: NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars. app_key: golang.no-proxy - key: CaptureSymbols - description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib). + description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib). app_key: golang.capture-symbols + - key: CaptureSymbolsModules + description: 'CaptureSymbolsModules is a list of glob patterns (doublestar syntax, where ** crosses path separators and * does not) matched against go module paths. Matching modules get symbols in addition to whatever CaptureSymbols selects, so this can only widen the selection and never narrow it. It has no effect under the "none" scope. A trailing major version suffix is not part of a module''s identity, so a pattern matches with or without it: both "github.com/foo/bar" and "github.com/foo/*" select "github.com/foo/bar/v2". A pattern that spells out a suffix selects only that major version.' + app_key: golang.capture-symbols-modules catalogers: - ecosystem: go # MANUAL name: go-module-binary-cataloger # AUTO-GENERATED diff --git a/syft/pkg/cataloger/golang/config.go b/syft/pkg/cataloger/golang/config.go index 228cd67b1..3d69bd8d7 100644 --- a/syft/pkg/cataloger/golang/config.go +++ b/syft/pkg/cataloger/golang/config.go @@ -51,10 +51,20 @@ type CatalogerConfig struct { MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"` // CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are - // "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib). + // "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module + // under golang.org/x/), and "all" (all module packages plus stdlib). // app-config: golang.capture-symbols CaptureSymbols cataloging.SymbolScope `yaml:"capture-symbols" json:"capture-symbols" mapstructure:"capture-symbols"` + // CaptureSymbolsModules is a list of glob patterns (doublestar syntax, where ** crosses path separators + // and * does not) matched against go module paths. Matching modules get symbols in addition to whatever + // CaptureSymbols selects, so this can only widen the selection and never narrow it. It has no effect + // under the "none" scope. A trailing major version suffix is not part of a module's identity, so a + // pattern matches with or without it: both "github.com/foo/bar" and "github.com/foo/*" select + // "github.com/foo/bar/v2". A pattern that spells out a suffix selects only that major version. + // app-config: golang.capture-symbols-modules + CaptureSymbolsModules []string `yaml:"capture-symbols-modules,omitempty" json:"capture-symbols-modules,omitempty" mapstructure:"capture-symbols-modules"` + // Whether to use the golang.org/x/tools/go/packages, which executes golang tooling found on the path in addition to potential network access UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"` } @@ -196,6 +206,11 @@ func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) Catalo return g } +func (g CatalogerConfig) WithCaptureSymbolsModules(input []string) CatalogerConfig { + g.CaptureSymbolsModules = input + return g +} + func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig { g.UsePackagesLib = useLib return g diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index a4d24c918..f1a905673 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -21,7 +21,6 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" @@ -51,7 +50,7 @@ const devel = "(devel)" type goBinaryCataloger struct { licenseResolver goLicenseResolver mainModuleVersion MainModuleVersionConfig - symbolScope cataloging.SymbolScope + symbolSelector symbolSelector // stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the // binary's location), grouped by import path, populated during parsing and consumed by stdlibProcessor @@ -65,7 +64,7 @@ func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger { return &goBinaryCataloger{ licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts), mainModuleVersion: opts.MainModuleVersion, - symbolScope: opts.CaptureSymbols, + symbolSelector: newSymbolSelector(opts.CaptureSymbols, opts.CaptureSymbolsModules), stdlibSymbols: make(map[file.Coordinates]map[string][]string), } } @@ -121,7 +120,7 @@ func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Res } defer internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) - mods, errs := scanFile(reader.Location, unionReader, c.symbolScope != cataloging.SymbolScopeNone) + mods, errs := scanFile(reader.Location, unionReader, c.symbolSelector.enabled()) var rels []artifact.Relationship for _, mod := range mods { @@ -184,11 +183,10 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re symbolsByModule, stdlibSymbols := moduleSymbols(mod.symbols, &mod.Main, mod.Deps) c.recordStdlibSymbols(location.Coordinates, stdlibSymbols) - if c.symbolScope != cataloging.SymbolScopeAll { - // only the "all" scope attaches per-module symbols; for the "stdlib" scope we keep just the - // recorded stdlib symbols. nil map lookups below then yield nil symbol lists for each module. - symbolsByModule = nil - } + // keep only the modules the selector covers; the main module goes through the same map, so this + // incidentally decides the main module too (which is intended: it is treated like any dependency). + // unselected modules fall out entirely, so the lookups below yield nil rather than an empty map. + symbolsByModule = c.symbolSelector.filter(symbolsByModule) var pkgs []pkg.Package for _, dep := range mod.Deps { diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index 9881f91cc..14d03e206 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -4,12 +4,14 @@ import ( "bufio" "bytes" "context" + "encoding/json" "errors" "io" "os" "os/exec" "path/filepath" "runtime/debug" + "slices" "strconv" "strings" "syscall" @@ -1441,9 +1443,12 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) { tests := []struct { name string scope cataloging.SymbolScope + modules []string + extraDeps []*debug.Module symbols []binarySymbol wantMainSyms map[string][]string wantDepSyms map[string][]string + wantExtraSyms []map[string][]string wantStdlibSyms map[string][]string }{ { @@ -1468,6 +1473,84 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) { }, wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, }, + { + // golang.org/x/net is reached through a vendored import path here, which must still be + // attributed to (and selected by) the module that owns it + name: "extended-stdlib captures golang.org/x modules and stdlib only", + scope: cataloging.SymbolScopeExtendedStdlib, + extraDeps: extendedDeps, + symbols: slices.Concat(populatedSymbols, extendedSymbols), + wantExtraSyms: []map[string][]string{ + {"golang.org/x/net/http2": {"NewClientConn"}}, + nil, + }, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, + }, + { + name: "module patterns widen extended-stdlib", + scope: cataloging.SymbolScopeExtendedStdlib, + modules: []string{"github.com/klauspost/**"}, + extraDeps: extendedDeps, + symbols: slices.Concat(populatedSymbols, extendedSymbols), + wantExtraSyms: []map[string][]string{ + {"golang.org/x/net/http2": {"NewClientConn"}}, + {"github.com/klauspost/compress/zstd": {"NewReader"}}, + }, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, + }, + { + name: "module patterns can select the main module", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/anchore/**"}, + extraDeps: extendedDeps, + symbols: slices.Concat(populatedSymbols, extendedSymbols), + // the main package is keyed by the "main" import path the linker assigns, not its real path + wantMainSyms: map[string][]string{"main": {"main"}}, + wantExtraSyms: []map[string][]string{nil, nil}, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, + }, + { + name: "module patterns are inert under the none scope", + scope: cataloging.SymbolScopeNone, + modules: []string{"github.com/**", "golang.org/x/**"}, + extraDeps: extendedDeps, + // scanFile never runs under "none", so the build info carries no symbols to begin with + symbols: nil, + wantExtraSyms: []map[string][]string{nil, nil}, + }, + { + // all short-circuits ahead of the pattern walk, so a narrow module pattern list cannot subtract + // from it. this is also the guard on that short-circuit still existing. + name: "module patterns cannot narrow the all scope", + scope: cataloging.SymbolScopeAll, + modules: []string{"golang.org/x/**"}, + extraDeps: extendedDeps, + symbols: slices.Concat(populatedSymbols, extendedSymbols), + wantMainSyms: map[string][]string{"main": {"main"}}, + wantDepSyms: map[string][]string{ + "github.com/foo/bar": {"Parse"}, + "github.com/foo/bar/baz": {"Helper"}, + }, + wantExtraSyms: []map[string][]string{ + {"golang.org/x/net/http2": {"NewClientConn"}}, + {"github.com/klauspost/compress/zstd": {"NewReader"}}, + }, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, + }, + { + // a module pattern that restates what the preset already covers must not duplicate or drop anything: + // selection is a per-module boolean, so overlap is idempotent + name: "a module pattern overlapping the preset changes nothing", + scope: cataloging.SymbolScopeExtendedStdlib, + modules: []string{"golang.org/x/**"}, + extraDeps: extendedDeps, + symbols: slices.Concat(populatedSymbols, extendedSymbols), + wantExtraSyms: []map[string][]string{ + {"golang.org/x/net/http2": {"NewClientConn"}}, + nil, + }, + wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}}, + }, } for _, tt := range tests { @@ -1476,27 +1559,52 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) { BuildInfo: &debug.BuildInfo{ GoVersion: "go1.22.0", Main: debug.Module{Path: "github.com/anchore/syft", Version: "v1.0.0"}, - Deps: []*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}}, + Deps: append([]*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}}, tt.extraDeps...), }, arch: "amd64", symbols: tt.symbols, } - c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope}) + c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope, CaptureSymbolsModules: tt.modules}) reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(""))) require.NoError(t, err) mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), fileresolver.Empty{}, location, mod, mod.arch, reader) require.NotNil(t, mainPkg) - require.Len(t, pkgs, 1) + require.Len(t, pkgs, 1+len(tt.extraDeps)) assert.Equal(t, tt.wantMainSyms, mainPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "main module symbols") assert.Equal(t, tt.wantDepSyms, pkgs[0].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "dependency symbols") + for i, want := range tt.wantExtraSyms { + assert.Equal(t, want, pkgs[1+i].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "symbols for %s", pkgs[1+i].Name) + } assert.Equal(t, tt.wantStdlibSyms, c.stdlibSymbolsFor(location.Coordinates), "recorded stdlib symbols") + + // a module selected by nothing must carry no symbols key at all: assert on the serialized form, + // since an empty (non-nil) map would still emit "symbols":{} despite the omitempty tag + for _, p := range append([]pkg.Package{*mainPkg}, pkgs...) { + encoded, err := json.Marshal(p.Metadata) + require.NoError(t, err) + if p.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols == nil { + assert.NotContains(t, string(encoded), `"symbols"`, "expected no symbols key for %s", p.Name) + } + } }) } } +var ( + extendedDeps = []*debug.Module{ + {Path: "golang.org/x/net", Version: "v0.30.0"}, + {Path: "github.com/klauspost/compress", Version: "v1.17.0"}, + } + + extendedSymbols = []binarySymbol{ + {packagePath: "vendor/golang.org/x/net/http2", name: "vendor/golang.org/x/net/http2.NewClientConn"}, + {packagePath: "github.com/klauspost/compress/zstd", name: "github.com/klauspost/compress/zstd.NewReader"}, + } +) + // Test_recordStdlibSymbols_merge covers the merge path where the same binary location records stdlib // symbols more than once. This happens in production for universal/fat Mach-O binaries: scanFile yields // one build info per architecture and each is recorded under the same location coordinates. diff --git a/syft/pkg/cataloger/golang/symbol_selector.go b/syft/pkg/cataloger/golang/symbol_selector.go new file mode 100644 index 000000000..59333a9c5 --- /dev/null +++ b/syft/pkg/cataloger/golang/symbol_selector.go @@ -0,0 +1,113 @@ +package golang + +import ( + "slices" + + "github.com/bmatcuk/doublestar/v4" + "golang.org/x/mod/module" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/cataloging" +) + +// scopePatterns maps a capture-symbols scope onto the go module path globs it selects. The "none" and +// "all" scopes are intentionally absent: both are answered without matching. The "stdlib" scope selects +// no modules at all (the synthetic stdlib package is not a module and is handled separately). +var scopePatterns = map[cataloging.SymbolScope][]string{ + cataloging.SymbolScopeExtendedStdlib: {"golang.org/x/**"}, +} + +// symbolSelector decides which go module paths get function symbols attached to their metadata. The scope +// preset and any user-supplied module patterns are compiled into a single glob list so there is exactly +// one matcher answering "does this module path get symbols" (two matchers over the same subject drift). +type symbolSelector struct { + scope cataloging.SymbolScope + patterns []string +} + +func newSymbolSelector(scope cataloging.SymbolScope, modules []string) symbolSelector { + // normalize here rather than trusting the caller: the CLI runs Parse in PostLoad, but a library + // consumer setting CaptureSymbols directly (or via WithCaptureSymbols) does not, and an unnormalized + // value falls through both switches below into stdlib-only capture rather than the intended scope. + scope = scope.Parse() + + var patterns []string + for _, pattern := range slices.Concat(scopePatterns[scope], modules) { + if !doublestar.ValidatePattern(pattern) { + // a typo in a filter that decides what gets vulnerability-scanned must not pass quietly: + // someone would believe they captured symbols they did not. Drop just this pattern and keep + // the rest of the selection in force. + log.WithFields("pattern", pattern).Warn("ignoring malformed golang capture-symbols-modules pattern") + continue + } + patterns = append(patterns, pattern) + } + return symbolSelector{scope: scope, patterns: patterns} +} + +// enabled reports whether symbols should be extracted from the binary at all. +func (s symbolSelector) enabled() bool { + return s.scope != cataloging.SymbolScopeNone +} + +// selects reports whether the given go module path gets symbols. The binary's own main module is treated +// exactly like a dependency. +func (s symbolSelector) selects(modulePath string) bool { + switch s.scope { + case cataloging.SymbolScopeNone: + return false + case cataloging.SymbolScopeAll: + return true + } + + // a major version suffix is part of a module's path but not part of its identity, so patterns are matched + // against the path both with and without it: `github.com/foo/bar` and `github.com/foo/*` each select + // `github.com/foo/bar/v2`, which is what whoever wrote either one meant, and a config does not quietly + // stop covering a module the day it bumps a major version. Spelling a suffix out in the pattern still + // selects that major version alone, since the unsuffixed path cannot match a pattern carrying one. + // Only a trailing suffix is a version: in `github.com/foo/v2/bar` the `v2` is an ordinary path element, + // and SplitPathVersion leaves it there. + unversioned, major, ok := module.SplitPathVersion(modulePath) + versioned := ok && major != "" + + for _, pattern := range s.patterns { + if globMatches(pattern, modulePath) { + return true + } + if versioned && globMatches(pattern, unversioned) { + return true + } + } + return false +} + +// globMatches treats a pattern that cannot compile as no match, which newSymbolSelector has already warned about. +func globMatches(pattern, modulePath string) bool { + matched, err := doublestar.Match(pattern, modulePath) + if err != nil { + // unreachable: newSymbolSelector rejects (and warns about) patterns that cannot compile + return false + } + return matched +} + +// filter drops the entries of a module-path-keyed symbol map that the selector does not select. It returns +// nil when nothing is selected so callers attach no symbols map at all (rather than an empty one, which +// would defeat the omitempty JSON tag). +func (s symbolSelector) filter(byModule map[string]map[string][]string) map[string]map[string][]string { + switch s.scope { + case cataloging.SymbolScopeNone: + return nil + case cataloging.SymbolScopeAll: + return byModule + } + for modulePath := range byModule { + if !s.selects(modulePath) { + delete(byModule, modulePath) + } + } + if len(byModule) == 0 { + return nil + } + return byModule +} diff --git a/syft/pkg/cataloger/golang/symbol_selector_test.go b/syft/pkg/cataloger/golang/symbol_selector_test.go new file mode 100644 index 000000000..b9f992465 --- /dev/null +++ b/syft/pkg/cataloger/golang/symbol_selector_test.go @@ -0,0 +1,280 @@ +package golang + +import ( + "maps" + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/cataloging" +) + +func Test_symbolSelector_selects(t *testing.T) { + tests := []struct { + name string + scope cataloging.SymbolScope + modules []string + modulePath string + want bool + }{ + { + name: "none selects nothing", + scope: cataloging.SymbolScopeNone, + modulePath: "github.com/foo/bar", + }, + { + name: "none is inert even with module patterns", + scope: cataloging.SymbolScopeNone, + modules: []string{"github.com/foo/**"}, + modulePath: "github.com/foo/bar", + }, + { + name: "all selects everything", + scope: cataloging.SymbolScopeAll, + modulePath: "github.com/foo/bar", + want: true, + }, + { + name: "stdlib selects no modules", + scope: cataloging.SymbolScopeStdlib, + modulePath: "golang.org/x/crypto", + }, + { + name: "extended-stdlib matches a golang.org/x module", + scope: cataloging.SymbolScopeExtendedStdlib, + modulePath: "golang.org/x/crypto", + want: true, + }, + { + // ** crosses path separators, which is what lets one pattern cover the whole subtree + name: "extended-stdlib matches a nested golang.org/x module", + scope: cataloging.SymbolScopeExtendedStdlib, + modulePath: "golang.org/x/tools/gopls", + want: true, + }, + { + // the pattern must not match on a bare prefix, only at a path boundary + name: "extended-stdlib does not match golang.org/xtra", + scope: cataloging.SymbolScopeExtendedStdlib, + modulePath: "golang.org/xtra", + }, + { + name: "extended-stdlib does not match an unrelated module", + scope: cataloging.SymbolScopeExtendedStdlib, + modulePath: "github.com/klauspost/compress", + }, + { + name: "single star matches one path segment", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/*"}, + modulePath: "github.com/klauspost/compress", + want: true, + }, + { + // a single star does not cross "/", but a major version suffix is not a path segment for this + // purpose: the module is matched with the suffix stripped as well, so a config written before a + // major bump keeps covering the module after it + name: "single star reaches across a major version suffix", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/*"}, + modulePath: "github.com/klauspost/compress/v2", + want: true, + }, + { + name: "single star does not cross a separator that is not a version suffix", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/*"}, + modulePath: "github.com/klauspost/compress/internal/thing", + }, + { + // only a trailing suffix is a version. here "v2" is an ordinary path element naming the major + // subdirectory a nested module lives in, so it is matched literally and ** is the way to reach it + name: "a mid-path version-like element is an ordinary segment", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/anchore/*/thing"}, + modulePath: "github.com/anchore/syft/v2/thing", + }, + { + name: "doublestar reaches a mid-path version-like element", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/anchore/**/thing"}, + modulePath: "github.com/anchore/syft/v2/thing", + want: true, + }, + { + name: "doublestar crosses a separator", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/**"}, + modulePath: "github.com/klauspost/compress/v2", + want: true, + }, + { + name: "an exact path covers every major version of that module", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/compress"}, + modulePath: "github.com/klauspost/compress/v2", + want: true, + }, + { + // spelling the suffix out is how a single major version is targeted: v1's path carries no suffix, + // so there is nothing for a suffixed pattern to match + name: "a pattern naming a major version selects only that one", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/compress/v2"}, + modulePath: "github.com/klauspost/compress", + }, + { + name: "a pattern naming a major version selects it", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/compress/v2"}, + modulePath: "github.com/klauspost/compress/v2", + want: true, + }, + { + // gopkg.in spells the major version as a .vN suffix on the last element, which SplitPathVersion + // understands, so it strips the same way + name: "gopkg.in style suffixes strip too", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"gopkg.in/yaml"}, + modulePath: "gopkg.in/yaml.v2", + want: true, + }, + { + // /v1 and /v0 are not valid major version suffixes, so this is not a versioned path at all and + // nothing is stripped from it + name: "a v1 element is not a version suffix", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"github.com/klauspost/compress"}, + modulePath: "github.com/klauspost/compress/v1", + }, + { + name: "module patterns widen a preset", + scope: cataloging.SymbolScopeExtendedStdlib, + modules: []string{"github.com/klauspost/**"}, + modulePath: "github.com/klauspost/compress", + want: true, + }, + { + name: "module patterns cannot narrow a preset", + scope: cataloging.SymbolScopeExtendedStdlib, + modules: []string{"github.com/klauspost/**"}, + modulePath: "golang.org/x/net", + want: true, + }, + { + name: "exact module path", + scope: cataloging.SymbolScopeStdlib, + modules: []string{"google.golang.org/grpc"}, + modulePath: "google.golang.org/grpc", + want: true, + }, + { + name: "an empty module pattern list is a no-op", + scope: cataloging.SymbolScopeStdlib, + modules: []string{}, + modulePath: "github.com/foo/bar", + }, + { + // all short-circuits before any matching, so a module pattern list cannot subtract from it + name: "module patterns cannot narrow all", + scope: cataloging.SymbolScopeAll, + modules: []string{"github.com/klauspost/**"}, + modulePath: "github.com/foo/bar", + want: true, + }, + { + // selection is a per-module boolean, so a pattern overlapping the preset is idempotent + name: "a module pattern overlapping the preset is idempotent", + scope: cataloging.SymbolScopeExtendedStdlib, + modules: []string{"golang.org/x/**"}, + modulePath: "golang.org/x/net", + want: true, + }, + { + // library consumers set CaptureSymbols directly without going through PostLoad, so the + // selector normalizes rather than falling through to stdlib-only capture + name: "unnormalized scope is parsed", + scope: "All", + modulePath: "github.com/foo/bar", + want: true, + }, + { + name: "unrecognized scope resolves to none", + scope: "bogus", + modules: []string{"github.com/foo/**"}, + modulePath: "github.com/foo/bar", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, newSymbolSelector(tt.scope, tt.modules).selects(tt.modulePath)) + }) + } +} + +// a malformed pattern must be dropped at construction, leaving the remaining patterns in force rather +// than aborting the selection or silently matching nothing +func Test_symbolSelector_malformedPattern(t *testing.T) { + s := newSymbolSelector(cataloging.SymbolScopeExtendedStdlib, []string{"[", "github.com/klauspost/**"}) + + require.Equal(t, []string{"golang.org/x/**", "github.com/klauspost/**"}, s.patterns) + assert.True(t, s.selects("golang.org/x/net"), "preset still applies") + assert.True(t, s.selects("github.com/klauspost/compress"), "valid sibling pattern still applies") + assert.False(t, s.selects("github.com/foo"), "malformed pattern selects nothing") +} + +func Test_symbolSelector_filter(t *testing.T) { + symbols := func() map[string]map[string][]string { + return map[string]map[string][]string{ + "golang.org/x/net": {"golang.org/x/net/http2": {"NewClientConn"}}, + "github.com/klauspost/compress": {"github.com/klauspost/compress/zstd": {"NewReader"}}, + } + } + + tests := []struct { + name string + scope cataloging.SymbolScope + modules []string + want []string // remaining module paths + }{ + { + name: "none drops everything", + scope: cataloging.SymbolScopeNone, + }, + { + name: "stdlib drops every module", + scope: cataloging.SymbolScopeStdlib, + }, + { + name: "extended-stdlib keeps only golang.org/x", + scope: cataloging.SymbolScopeExtendedStdlib, + want: []string{"golang.org/x/net"}, + }, + { + name: "module patterns widen the selection", + scope: cataloging.SymbolScopeExtendedStdlib, + modules: []string{"github.com/klauspost/**"}, + want: []string{"golang.org/x/net", "github.com/klauspost/compress"}, + }, + { + name: "all keeps everything", + scope: cataloging.SymbolScopeAll, + want: []string{"golang.org/x/net", "github.com/klauspost/compress"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newSymbolSelector(tt.scope, tt.modules).filter(symbols()) + if len(tt.want) == 0 { + // nil, not an empty map, so the omitempty JSON tag keeps the field out of output + assert.Nil(t, got) + return + } + assert.ElementsMatch(t, tt.want, slices.Collect(maps.Keys(got))) + }) + } +} diff --git a/syft/pkg/golang.go b/syft/pkg/golang.go index 8b133a8de..60f374d6f 100644 --- a/syft/pkg/golang.go +++ b/syft/pkg/golang.go @@ -30,8 +30,10 @@ type GolangBinaryBuildinfoEntry struct { // name is the import path, a ".", and the local name. One exception: the binary's main package appears // under the key "main" (the name the linker assigns), not its original source import path, which is not // recoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers - // this package: the "all" scope populates every module package plus the synthetic stdlib package, while - // the "stdlib" scope populates only the stdlib package. + // this package: the "all" scope populates every module package plus the synthetic stdlib package, the + // "extended-stdlib" scope populates the stdlib package plus every module under golang.org/x/, and the + // "stdlib" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate + // any additional modules they match. Symbols map[string][]string `json:"symbols,omitempty"` } From d288dd67aefd7ea2631b8d22bb9616637b380a5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:30:56 +0000 Subject: [PATCH 118/168] chore(deps): bump docker/login-action from 4.5.1 to 4.6.0 (#5157) Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7...dbcb813823bdd20940b903addbd779551569679f) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9310a16c1..8218feecf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -89,13 +89,13 @@ jobs: uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Login to Docker Hub - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 #v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0 with: username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }} password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }} - name: Login to GitHub Container Registry - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 #v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} From 5f67883fe87e7fd5383c2835b3c225acf5240a35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:31:31 +0000 Subject: [PATCH 119/168] chore(deps): bump go.yaml.in/yaml/v3 from 3.0.4 to 3.0.5 (#5158) Bumps [go.yaml.in/yaml/v3](https://github.com/yaml/go-yaml) from 3.0.4 to 3.0.5. - [Commits](https://github.com/yaml/go-yaml/compare/v3.0.4...v3.0.5) --- updated-dependencies: - dependency-name: go.yaml.in/yaml/v3 dependency-version: 3.0.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 31eead8c6..1b337de9a 100644 --- a/go.mod +++ b/go.mod @@ -91,7 +91,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 go.uber.org/goleak v1.3.0 - go.yaml.in/yaml/v3 v3.0.4 + go.yaml.in/yaml/v3 v3.0.5 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/mod v0.38.0 golang.org/x/net v0.57.0 diff --git a/go.sum b/go.sum index 7d541412d..490d2a19d 100644 --- a/go.sum +++ b/go.sum @@ -1022,8 +1022,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= From 0fe98ed1f2990ed93ce7e973090207eb95654f3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:32:31 +0000 Subject: [PATCH 120/168] chore(deps): bump github.com/go-git/go-git/v5 from 5.19.1 to 5.19.2 (#5159) Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.19.1 to 5.19.2. - [Release notes](https://github.com/go-git/go-git/releases) - [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md) - [Commits](https://github.com/go-git/go-git/compare/v5.19.1...v5.19.2) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1b337de9a..9f236fbdd 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/github/go-spdx/v2 v2.7.0 github.com/gkampitakis/go-snaps v0.5.23 github.com/go-git/go-billy/v5 v5.9.1 - github.com/go-git/go-git/v5 v5.19.1 + github.com/go-git/go-git/v5 v5.19.2 github.com/go-test/deep v1.1.1 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/goccy/go-yaml v1.19.2 diff --git a/go.sum b/go.sum index 490d2a19d..9637c3252 100644 --- a/go.sum +++ b/go.sum @@ -422,8 +422,8 @@ github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryL github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= -github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= +github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= From f45586b4577e748ad2122dd5c72c28a7536bdddc Mon Sep 17 00:00:00 2001 From: Timo <57227498+EchterTimo@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:31:27 +0200 Subject: [PATCH 121/168] Remove Go Report Card badge from README (#5130) Signed-off-by: Timo <57227498+EchterTimo@users.noreply.github.com> --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index acf888e34..d864e1428 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@

 Validations  -  Go Report Card   GitHub release   GitHub go.mod Go version   License: Apache-2.0  From 07fb23487f9e2adb19ffcd8f589fe9881e0c86b7 Mon Sep 17 00:00:00 2001 From: Gunny Patel Date: Fri, 7 Aug 2026 10:36:09 -0400 Subject: [PATCH 122/168] feat(golang): detect native Go FIPS 140 mode in binaries (#5155) Signed-off-by: Gunny Patel --- syft/pkg/cataloger/golang/scan_binary.go | 21 +++++++++ syft/pkg/cataloger/golang/scan_binary_test.go | 47 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/syft/pkg/cataloger/golang/scan_binary.go b/syft/pkg/cataloger/golang/scan_binary.go index 84fd6d2b2..a4841a27c 100644 --- a/syft/pkg/cataloger/golang/scan_binary.go +++ b/syft/pkg/cataloger/golang/scan_binary.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "runtime/debug" + "strings" "github.com/kastenhq/goversion/version" @@ -51,6 +52,7 @@ func scanFile(location file.Location, reader unionreader.UnionReader, captureSym // we can still catalog packages, even if we can't get the crypto information errs = unknown.Appendf(errs, location, "unable to read golang version info: %w", err) } + v = append(v, getNativeFIPSSettings(bi.Settings)...) arch := getGOARCH(bi.Settings) if arch == "" { arch, err = getGOARCHFromBin(r) @@ -101,6 +103,25 @@ func getCryptoSettingsFromVersion(v version.Version) []string { return cryptoSettings } +func getNativeFIPSSettings(settings []debug.BuildSetting) []string { + var cryptoSettings []string + for _, s := range settings { + switch s.Key { + case "GOFIPS140": + if s.Value != "" { + cryptoSettings = append(cryptoSettings, "GOFIPS140="+s.Value) + } + case "DefaultGODEBUG": + for _, kv := range strings.Split(s.Value, ",") { + if setting, val, ok := strings.Cut(kv, "="); ok && setting == "fips140" { + cryptoSettings = append(cryptoSettings, "GODEBUG=fips140="+val) + } + } + } + } + return cryptoSettings +} + func getBuildInfo(r io.ReaderAt, location file.Location) (bi *debug.BuildInfo, err error) { defer func() { if r := recover(); r != nil { diff --git a/syft/pkg/cataloger/golang/scan_binary_test.go b/syft/pkg/cataloger/golang/scan_binary_test.go index 7595ae210..1e6910e6d 100644 --- a/syft/pkg/cataloger/golang/scan_binary_test.go +++ b/syft/pkg/cataloger/golang/scan_binary_test.go @@ -110,3 +110,50 @@ func Test_getCryptoSettingsFromVersion(t *testing.T) { }) } } + +func Test_getNativeFIPSSettings(t *testing.T) { + for _, tt := range []struct { + name string + settings []debug.BuildSetting + result []string + }{ + { + name: "not set", + settings: []debug.BuildSetting{{Key: "GOARCH", Value: "arm64"}}, + result: nil, + }, + { + name: "GOFIPS140=off is reported verbatim", + settings: []debug.BuildSetting{{Key: "GOFIPS140", Value: "off"}}, + result: []string{"GOFIPS140=off"}, + }, + { + name: "pinned module version with mode on", + settings: []debug.BuildSetting{ + {Key: "GOFIPS140", Value: "v1.0.0"}, + {Key: "DefaultGODEBUG", Value: "fips140=on"}, + }, + result: []string{"GOFIPS140=v1.0.0", "GODEBUG=fips140=on"}, + }, + { // GOFIPS140=latest enables FIPS mode without pinning a module version + name: "latest with mode on", + settings: []debug.BuildSetting{ + {Key: "GOFIPS140", Value: "latest"}, + {Key: "DefaultGODEBUG", Value: "fips140=on"}, + }, + result: []string{"GOFIPS140=latest", "GODEBUG=fips140=on"}, + }, + { // fips140 is one entry among many in DefaultGODEBUG + name: "fips140 among other godebug defaults", + settings: []debug.BuildSetting{ + {Key: "DefaultGODEBUG", Value: "asynctimerchan=1,fips140=on,tlssha1=1"}, + }, + result: []string{"GODEBUG=fips140=on"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + res := getNativeFIPSSettings(tt.settings) + assert.ElementsMatch(t, res, tt.result) + }) + } +} From 949ac703694941369ea979f4cd6172097a2823da Mon Sep 17 00:00:00 2001 From: Ankit Date: Fri, 7 Aug 2026 20:08:14 +0530 Subject: [PATCH 123/168] fix(java): map legacy Jackson 1.x (-asl) artifacts to org.codehaus.jackson (#5146) jackson-mapper-asl, jackson-core-asl, and their sibling artifacts (jackson-jaxrs, jackson-xc, jackson-smile) predate the convention of embedding META-INF/maven/.../pom.properties in the jar (they were built with Ant before ~2014). With no POM metadata to read, syft's groupIDFromJavaMetadata falls through to using the artifact name itself as the group ID, e.g. pkg:maven/jackson-mapper-asl/jackson-mapper-asl@1.9.13 instead of the correct pkg:maven/org.codehaus.jackson/jackson-mapper-asl@1.9.13 (confirmed against the published POM on Maven Central for all five artifacts). Because the generated purl's namespace doesn't match the vulnerability database's namespace for these packages, this causes false negatives in downstream scanning (e.g. Grype cannot match known CVEs such as CVE-2019-10202 against jackson-mapper-asl). Add the five artifacts to DefaultArtifactIDToGroupID, the same known- package-list fallback already used for other jars with incomplete metadata (e.g. the existing ant-*, spring-ldap* entries). Fixes #4598 Signed-off-by: ankit090701 --- .../internal/cpegenerate/java_groupid_map.go | 12 ++++++++++++ syft/pkg/cataloger/java/package_url_test.go | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go index 52c8ad250..0e4aa8abd 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go +++ b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go @@ -1921,4 +1921,16 @@ var DefaultArtifactIDToGroupID = map[string]string{ "kafka_2.8.2": "org.apache.kafka", "kafka_2.9.1": "org.apache.kafka", "kafka_2.9.2": "org.apache.kafka", + + // legacy "Jackson 1.x" (aka "jackson-asl") artifacts predate the convention of embedding + // META-INF/maven/.../pom.properties in the jar, so groupIDFromKnownPackageList is the only + // way to recover the correct group ID for jars built without that metadata (e.g. Ant-built + // jars from before ~2014). Without this, the group ID falls back to the artifact name itself, + // producing purls that do not match the vulnerability database's namespace. + // See https://github.com/anchore/syft/issues/4598 + "jackson-core-asl": "org.codehaus.jackson", + "jackson-mapper-asl": "org.codehaus.jackson", + "jackson-jaxrs": "org.codehaus.jackson", + "jackson-xc": "org.codehaus.jackson", + "jackson-smile": "org.codehaus.jackson", } diff --git a/syft/pkg/cataloger/java/package_url_test.go b/syft/pkg/cataloger/java/package_url_test.go index 0e0532ffc..5433b1481 100644 --- a/syft/pkg/cataloger/java/package_url_test.go +++ b/syft/pkg/cataloger/java/package_url_test.go @@ -185,6 +185,23 @@ func Test_groupIDFromJavaMetadata(t *testing.T) { metadata: pkg.JavaArchive{}, expect: "org.springframework.ldap", }, + { + // regression for github.com/anchore/syft/issues/4598: legacy Jackson 1.x ("-asl") + // jars built before ~2014 have no embedded pom.properties, so without the known + // package list the group ID falls back to the artifact name itself, producing a + // purl that doesn't match the vulnerability database's namespace (e.g. the correct + // group for jackson-mapper-asl is org.codehaus.jackson, not jackson-mapper-asl). + name: "known package list jackson-mapper-asl", + pkgName: "jackson-mapper-asl", + metadata: pkg.JavaArchive{}, + expect: "org.codehaus.jackson", + }, + { + name: "known package list jackson-core-asl", + pkgName: "jackson-core-asl", + metadata: pkg.JavaArchive{}, + expect: "org.codehaus.jackson", + }, { name: "java manifest", metadata: pkg.JavaArchive{ From 9dd9ce0bddeae2ff45795bf6718ea57db07c7d60 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 10 Aug 2026 10:24:51 -0400 Subject: [PATCH 124/168] chore(deps): update CPE dictionary index (#5166) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 6c01aaf22..a61f11642 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -112,6 +112,9 @@ "github.com/gookit/goutil": [ "cpe:2.3:a:go_util_project:go_util:*:*:*:*:*:go:*:*" ], + "github.com/gopacket/gopacket": [ + "cpe:2.3:a:gopacket:gopacket:*:*:*:*:*:go:*:*" + ], "github.com/hamba/avro/v2": [ "cpe:2.3:a:avro_project:avro:*:*:*:*:*:go:*:*" ], @@ -2502,13 +2505,20 @@ "@fastify/express": [ "cpe:2.3:a:fastify:fastify\\/express:*:*:*:*:*:node.js:*:*" ], + "@fastify/forwarded": [ + "cpe:2.3:a:fastify:fastify\\/forwarded:*:*:*:*:*:node.js:*:*" + ], "@fastify/http-proxy": [ "cpe:2.3:a:fastify:fastify\\/http-proxy:*:*:*:*:*:node.js:*:*" ], "@fastify/middie": [ "cpe:2.3:a:fastify:fastify\\/middie:*:*:*:*:*:node.js:*:*" ], + "@fastify/rate-limit": [ + "cpe:2.3:a:fastify:fastify\\/rate-limit:*:*:*:*:*:node.js:*:*" + ], "@fastify/reply-from": [ + "cpe:2.3:a:fastify:fastify\\/reply-from:*:*:*:*:*:node.js:*:*", "cpe:2.3:a:fastify:reply-from:*:*:*:*:*:node.js:*:*" ], "@fastly/js-compute": [ @@ -2649,6 +2659,9 @@ "@openzeppelin/contracts-upgradeable": [ "cpe:2.3:a:openzeppelin:contracts_upgradeable:*:*:*:*:*:node.js:*:*" ], + "@paperclipai/server": [ + "cpe:2.3:a:paperclip:paperclipai\\/server:*:*:*:*:*:node.js:*:*" + ], "@parse/push-adapter": [ "cpe:2.3:a:parseplatform:parse_server_push_adapter:*:*:*:*:*:node.js:*:*" ], @@ -3101,6 +3114,9 @@ "clawdbot": [ "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*" ], + "clawvet": [ + "cpe:2.3:a:mohibshaikh:clawvet:*:*:*:*:*:node.js:*:*" + ], "cli": [ "cpe:2.3:a:cli_project:cli:*:*:*:*:*:node.js:*:*" ], @@ -5124,6 +5140,9 @@ "pandora-doomsday": [ "cpe:2.3:a:pandora-doomsday_project:pandora-doomsday:*:*:*:*:*:node.js:*:*" ], + "paperclipai": [ + "cpe:2.3:a:paperclip:paperclipai:*:*:*:*:*:node.js:*:*" + ], "parse-dashboard": [ "cpe:2.3:a:parseplatform:parse_dashboard:*:*:*:*:*:node.js:*:*" ], @@ -5673,6 +5692,9 @@ "shout": [ "cpe:2.3:a:shout_project:shout:*:*:*:*:*:node.js:*:*" ], + "sigstore": [ + "cpe:2.3:a:sigstore:sigstore:*:*:*:*:*:node.js:*:*" + ], "sillytavern": [ "cpe:2.3:a:sillytavern:sillytavern:*:*:*:*:*:node.js:*:*" ], @@ -6112,6 +6134,9 @@ "vite": [ "cpe:2.3:a:vitejs:vite:*:*:*:*:*:node.js:*:*" ], + "vitest": [ + "cpe:2.3:a:vitest.dev:vitest:*:*:*:*:*:node.js:*:*" + ], "vm2": [ "cpe:2.3:a:vm2_project:vm2:*:*:*:*:*:node.js:*:*" ], @@ -6539,6 +6564,9 @@ "dash-uploader": [ "cpe:2.3:a:fohrloop:dash-uploader:*:*:*:*:*:python:*:*" ], + "datamodel-code-generator": [ + "cpe:2.3:a:koxudaxi:datamodel-code-generator:*:*:*:*:*:python:*:*" + ], "datapizza-ai": [ "cpe:2.3:a:datapizza:datapizza_ai:*:*:*:*:*:*:*:*" ], @@ -6738,6 +6766,9 @@ "mpxj": [ "cpe:2.3:a:mpxj:mpxj:*:*:*:*:*:python:*:*" ], + "msgpack": [ + "cpe:2.3:a:msgpack:messagepack:*:*:*:*:*:python:*:*" + ], "nanobot-ai": [ "cpe:2.3:a:nanobot:nanobot:*:*:*:*:*:python:*:*" ], @@ -7349,6 +7380,9 @@ "mpxj": [ "cpe:2.3:a:mpxj:mpxj:*:*:*:*:*:ruby:*:*" ], + "msgpack": [ + "cpe:2.3:a:msgpack:messagepack:*:*:*:*:*:ruby:*:*" + ], "net-imap": [ "cpe:2.3:a:ruby-lang:net\\:\\:imap:*:*:*:*:*:ruby:*:*" ], @@ -7583,6 +7617,9 @@ "webrick": [ "cpe:2.3:a:ruby-lang:webrick:*:*:*:*:*:ruby:*:*" ], + "websocket-driver": [ + "cpe:2.3:a:faye:websocket-driver:*:*:*:*:*:ruby:*:*" + ], "websocket-extensions": [ "cpe:2.3:a:websocket-extensions_project:websocket-extensions:*:*:*:*:*:ruby:*:*" ], @@ -8002,6 +8039,9 @@ "openssl": [ "cpe:2.3:a:sfackler:openssl:*:*:*:*:*:rust:*:*" ], + "opentelemetry": [ + "cpe:2.3:a:opentelemetry:opentelemetry:*:*:*:*:*:rust:*:*" + ], "outer_cgi": [ "cpe:2.3:a:outer_cgi_project:outer_cgi:*:*:*:*:*:rust:*:*" ], @@ -8032,6 +8072,9 @@ "postscript": [ "cpe:2.3:a:postscript_project:postscript:*:*:*:*:*:rust:*:*" ], + "quiche": [ + "cpe:2.3:a:cloudflare:quiche:*:*:*:*:*:rust:*:*" + ], "quinn": [ "cpe:2.3:a:quinn_project:quinn:*:*:*:*:*:rust:*:*" ], @@ -9374,6 +9417,9 @@ "autoptimize": [ "cpe:2.3:a:autoptimize:autoptimize:*:*:*:*:*:wordpress:*:*" ], + "autoship-cloud": [ + "cpe:2.3:a:patternsinthecloud:autoship_cloud_for_woocommerce:*:*:*:*:*:wordpress:*:*" + ], "autotitle-for-wordpress": [ "cpe:2.3:a:unalignedcode:autotitle:*:*:*:*:*:wordpress:*:*" ], @@ -13044,6 +13090,9 @@ "fotobook": [ "cpe:2.3:a:fotobook_project:fotobook:*:*:*:*:*:wordpress:*:*" ], + "foxypress": [ + "cpe:2.3:a:webmovementllc:foxypress:*:*:*:*:*:wordpress:*:*" + ], "foxyshop": [ "cpe:2.3:a:foxy:foxyshop:*:*:*:*:*:wordpress:*:*" ], @@ -15797,6 +15846,9 @@ "my-geo-posts-free": [ "cpe:2.3:a:mindstien:my_geo_posts_free:*:*:*:*:*:wordpress:*:*" ], + "my-quota": [ + "cpe:2.3:a:dropstr:my_quota:*:*:*:*:*:wordpress:*:*" + ], "my-tickets": [ "cpe:2.3:a:my_tickets_project:my_tickets:*:*:*:*:*:wordpress:*:*" ], @@ -20842,6 +20894,9 @@ "woo-related-products-refresh-on-reload": [ "cpe:2.3:a:peachpay:related_products_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], + "woo-rfq-for-woocommerce": [ + "cpe:2.3:a:neahplugins:np_quote_request_for_woocommerce:*:*:*:*:*:wordpress:*:*" + ], "woo-save-abandoned-carts": [ "cpe:2.3:a:majas-lapu-izstrade:cartbounty:*:*:*:*:*:wordpress:*:*" ], @@ -22964,6 +23019,7 @@ "cpe:2.3:a:ydesignservices:yds_support_ticket_system:*:*:*:*:*:wordpress:*:*" ], "yellow-pencil-visual-theme-customizer": [ + "cpe:2.3:a:waspthemes:yellowpencil:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:yellowpencil:visual_css_style_editor:*:*:*:*:*:wordpress:*:*" ], "yellow-yard": [ @@ -23398,6 +23454,9 @@ "hello-agency": [ "cpe:2.3:a:cozythemes:hello_agency:*:*:*:*:*:wordpress:*:*" ], + "himalayas": [ + "cpe:2.3:a:themegrill:himalayas:*:*:*:*:*:wordpress:*:*" + ], "homey": [ "cpe:2.3:a:favethemes:homey:*:*:*:*:*:wordpress:*:*" ], From 2293641e3bd628a01bb37639318d62c0ebe89b39 Mon Sep 17 00:00:00 2001 From: Weston Steimel Date: Mon, 10 Aug 2026 15:26:29 +0100 Subject: [PATCH 125/168] fix: add correct CPE vendor/product candidates for Git for Windows PE binary (#5156) Signed-off-by: Weston Steimel --- .../cataloger/internal/cpegenerate/candidate_for_pe.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go index 00a032c5a..c5e29a897 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go +++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go @@ -32,6 +32,11 @@ func candidateVendorsForPE(p pkg.Package) fieldCandidateSet { candidates.addValue("artifex") } + if product == "git" || fileDesc == "git setup" || company == "the git development community" { + candidates.addValue("git_for_windows_project") + candidates.addValue("gitforwindows") + } + return candidates } @@ -59,5 +64,10 @@ func candidateProductsForPE(p pkg.Package) fieldCandidateSet { candidates.addValue("ghostscript") } + if product == "git" || fileDesc == "git setup" { + candidates.addValue("git_for_windows") + candidates.addValue("git") + } + return candidates } From 4782a94029a9e3c4b323c582095de84e99d76dd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:33:38 +0000 Subject: [PATCH 126/168] chore(deps): bump github.com/klauspost/compress from 1.19.1 to 1.19.2 (#5182) Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.1 to 1.19.2. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.19.1...v1.19.2) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f236fbdd..9f728a650 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 - github.com/klauspost/compress v1.19.1 + github.com/klauspost/compress v1.19.2 github.com/magiconair/properties v1.18.11 github.com/mholt/archives v0.1.5 github.com/moby/sys/mountinfo v0.7.2 diff --git a/go.sum b/go.sum index 9637c3252..8c1ca854a 100644 --- a/go.sum +++ b/go.sum @@ -645,8 +645,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= -github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= From 20cc3df3877f0ad263388b0efc9887854cb06173 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:33:58 +0000 Subject: [PATCH 127/168] chore(deps): bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2 (#5181) Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/6fc4b006235f201fdab3722e17240ab420d580e5...3dc1ecc9bcb9e94e9b2c709687979e1298497054) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/validate-github-actions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml index 4df815645..92d1a0da1 100644 --- a/.github/workflows/validate-github-actions.yaml +++ b/.github/workflows/validate-github-actions.yaml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: "Run zizmor" - uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 with: # there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default) advanced-security: true From 3eee102f63a480c4a7d43d0918109a74f606da8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:34:24 +0000 Subject: [PATCH 128/168] chore(deps): bump github.com/google/go-containerregistry (#5184) Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.21.7 to 0.21.9. - [Release notes](https://github.com/google/go-containerregistry/releases) - [Commits](https://github.com/google/go-containerregistry/compare/v0.21.7...v0.21.9) --- updated-dependencies: - dependency-name: github.com/google/go-containerregistry dependency-version: 0.21.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 9f728a650..d401576ae 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/goccy/go-yaml v1.19.2 github.com/gohugoio/hashstructure v0.6.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-containerregistry v0.21.7 + github.com/google/go-containerregistry v0.21.9 github.com/google/licensecheck v0.3.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.1 @@ -183,7 +183,7 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v29.6.1+incompatible // indirect + github.com/docker/cli v29.6.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -239,7 +239,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/moby/api v1.55.0 // indirect - github.com/moby/moby/client v0.5.0 // indirect + github.com/moby/moby/client v0.5.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/user v0.4.0 // indirect diff --git a/go.sum b/go.sum index 8c1ca854a..bb3985159 100644 --- a/go.sum +++ b/go.sum @@ -339,8 +339,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8= -github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= +github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -509,8 +509,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= -github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= +github.com/google/go-containerregistry v0.21.9 h1:F+D4uZ3iA3DLMJLfhaqMdHJbzeqm/216WGQq2dokuLs= +github.com/google/go-containerregistry v0.21.9/go.mod h1:dP5XNKcL7kMFF/TB3LfvWmVhAcv7iqkHb3oDK8aauTo= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= @@ -725,8 +725,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= -github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw= +github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= From d63c774fa944917fcd3bb17cd6554c45aef44867 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:34:28 +0000 Subject: [PATCH 129/168] chore(deps): bump modernc.org/sqlite from 1.55.0 to 1.56.0 (#5183) Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.55.0 to 1.56.0. - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.55.0...v1.56.0) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index d401576ae..c01e26d99 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.48.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.55.0 + modernc.org/sqlite v1.56.0 ) require ( @@ -205,7 +205,7 @@ require ( github.com/go-restruct/restruct v1.2.0-alpha // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect + github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect @@ -226,7 +226,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/maruel/natural v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect @@ -324,7 +324,7 @@ require ( google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - modernc.org/libc v1.74.1 // indirect + modernc.org/libc v1.74.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index bb3985159..696231d2b 100644 --- a/go.sum +++ b/go.sum @@ -537,8 +537,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= @@ -688,8 +688,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= @@ -1514,8 +1514,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= -modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= @@ -1526,8 +1526,8 @@ modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= -modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= +modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1536,8 +1536,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM= -modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= +modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 04a6fa1b412f36eee09fc439818e819d70cad3bd Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 14 Aug 2026 14:09:10 -0400 Subject: [PATCH 130/168] fix(unionreader): honor the io.ReaderAt contract in readerAtAdapter (#5186) `readerAtAdapter.ReadAt` seeks and then issues a single `Read`, which breaks the `io.ReaderAt` contract in both directions against the squashfs reader it exists to wrap: - `squashfs.File.Read` copies against the decompressed block length but advances its block cursor by the nominal block size, so a block that decompresses short silently stops copying and returns fewer bytes with a nil error. `ReadAt` forbids that, and callers rely on it: anything decoding a fixed-size structure off the result gets zero padding it has no way to detect and parses it as real data. The GraalVM PE export table and the UPX block reader both size a buffer from a header field and then ignore `n` entirely, so a crafted image drives them straight through the padding. - a read landing exactly on the end of the file returns a *full* buffer paired with `io.EOF`. `bytes.Reader.ReadAt` returns nil there, and the callers that treat any error as fatal were written against that, so squashfs-resident binaries sized near a read boundary were being skipped outright. `io.ReadFull` normalizes both: it fills the buffer across short reads, and it clears the error once the buffer is full. A genuinely short tail is reported as `io.EOF`, which is what `ReadAt` implementations return at the end of a file, and what the buffering branch of `GetUnionReader` already returns. Affects squashfs-backed sources (snaps), so in practice the binary catalogers reading structure out of executables. Signed-off-by: Alex Goodman --- syft/internal/unionreader/union_reader.go | 14 ++- .../internal/unionreader/union_reader_test.go | 113 +++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/syft/internal/unionreader/union_reader.go b/syft/internal/unionreader/union_reader.go index 8c3a09cc7..9fed8a186 100644 --- a/syft/internal/unionreader/union_reader.go +++ b/syft/internal/unionreader/union_reader.go @@ -120,9 +120,19 @@ func (r *readerAtAdapter) ReadAt(p []byte, off int64) (n int, err error) { return 0, err } - n, err = r.ReadSeekCloser.Read(p) // read from that absolute position + // io.ReaderAt requires len(p) bytes or a non-nil error, and the underlying reader honors neither half of + // that: a squashfs block that decompresses short returns fewer bytes with err == nil, and a read landing + // exactly on the end of the file returns a full buffer alongside io.EOF. io.ReadFull normalizes both. + n, err = io.ReadFull(r.ReadSeekCloser, p) + // ReadAt reports a short read at the end of the file as io.EOF. compared by identity on purpose: + // io.ReadFull returns this sentinel bare, and errors.Is would also downgrade a wrapped one from the + // underlying reader, turning genuine stream corruption into a benign end of file + if err == io.ErrUnexpectedEOF { + err = io.EOF + } - // restore the position for the stateful read/seek operations + // restore the position for the stateful read/seek operations. a read error wins over a restore failure: + // callers compare against io.EOF, and masking it would strand them without the data they did get if restoreErr := r.restorePosition(currentPos); restoreErr != nil { if err == nil { err = restoreErr diff --git a/syft/internal/unionreader/union_reader_test.go b/syft/internal/unionreader/union_reader_test.go index b18b8fcc6..2c8f6d253 100644 --- a/syft/internal/unionreader/union_reader_test.go +++ b/syft/internal/unionreader/union_reader_test.go @@ -2,6 +2,7 @@ package unionreader import ( "bytes" + "errors" "io" "strings" "sync" @@ -120,12 +121,14 @@ func TestReaderAtAdapter_ReadAt(t *testing.T) { expectedStr: "", }, { + // io.ReaderAt requires a non-nil error whenever it returns fewer than len(p) bytes, so a + // buffer that runs off the end of the file reports io.EOF alongside what it did read name: "partial read", data: "Hello", offset: 2, bufSize: 10, expectedN: 3, - expectedErr: nil, + expectedErr: io.EOF, expectedStr: "llo", }, { @@ -335,3 +338,111 @@ func (r *readSeekCloser) Close() error { r.closed = true return nil } + +// scriptedReadSeeker replays a fixed sequence of Read results so tests can express the read shapes a +// squashfs-backed reader actually produces, none of which an io.ReaderAt may pass through to callers: +// a short count with a nil error, a full count paired with io.EOF, or a failure partway through a buffer. +type scriptedReadSeeker struct { + reads []scriptedRead + next int + offset int64 +} + +type scriptedRead struct { + data string + err error +} + +func (r *scriptedReadSeeker) Read(p []byte) (int, error) { + if r.next >= len(r.reads) { + return 0, io.EOF + } + read := r.reads[r.next] + r.next++ + n := copy(p, read.data) + r.offset += int64(n) + return n, read.err +} + +func (r *scriptedReadSeeker) Seek(offset int64, whence int) (int64, error) { + if whence == io.SeekCurrent { + return r.offset, nil + } + r.offset = offset + return offset, nil +} + +func (r *scriptedReadSeeker) Close() error { return nil } + +func TestReaderAtAdapter_ReadAtHonorsReaderAtContract(t *testing.T) { + errBoom := errors.New("boom") + + tests := []struct { + name string + reads []scriptedRead + bufSize int + expectedN int + expectedErr error + expectedStr string + }{ + { + // before io.ReadFull the caller got the first 3 bytes and a nil error, and any parser sizing a + // struct off the result read zero padding it had no way to detect + name: "fills the buffer across short reads", + reads: []scriptedRead{{data: "abc"}, {data: "def"}, {data: "ghi"}}, + bufSize: 9, + expectedN: 9, + expectedErr: nil, + expectedStr: "abcdefghi", + }, + { + // squashfs pairs io.EOF with a full buffer when a read lands exactly on the end of the file; + // bytes.Reader.ReadAt returns nil there, and callers that treat any error as fatal rely on it + name: "drops io.EOF when the read still filled the buffer", + reads: []scriptedRead{{data: "abc"}, {data: "def", err: io.EOF}}, + bufSize: 6, + expectedN: 6, + expectedErr: nil, + expectedStr: "abcdef", + }, + { + // a short tail must surface as io.EOF rather than io.ErrUnexpectedEOF so callers comparing + // against io.EOF keep working + name: "reports a short tail as io.EOF", + reads: []scriptedRead{{data: "abc"}, {data: "de", err: io.EOF}}, + bufSize: 10, + expectedN: 5, + expectedErr: io.EOF, + expectedStr: "abcde", + }, + { + // the io.ErrUnexpectedEOF remap must not swallow a real read failure, and the byte count has to + // survive it, otherwise callers cannot tell how much of the buffer is trustworthy + name: "propagates a failure partway through the buffer", + reads: []scriptedRead{{data: "abc"}, {data: "", err: errBoom}}, + bufSize: 10, + expectedN: 3, + expectedErr: errBoom, + expectedStr: "abc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := &scriptedReadSeeker{reads: tt.reads} + adapter := newReaderAtAdapter(reader) + + buf := make([]byte, tt.bufSize) + n, err := adapter.ReadAt(buf, 7) + + require.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, tt.expectedN, n) + assert.Equal(t, tt.expectedStr, string(buf[:n])) + + // the position must be restored even though the read spanned multiple underlying calls + pos, err := adapter.Seek(0, io.SeekCurrent) + require.NoError(t, err) + assert.Zero(t, pos) + }) + } +} From 58a033f92409e48811a1e7376e9a113d4163c661 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 14 Aug 2026 16:08:58 -0400 Subject: [PATCH 131/168] Prevent unnecessary allocations when parsing compressed ELF sections (#5187) * fix(elf): bound compressed ELF section reads `debug/elf` takes a section's decompressed size from that section's own compression header, and a highly compressible stream really does deliver the bytes that header promises, so `internal/saferio` does not help: it faithfully allocates every one of them. A 2MB input file drives `elf.NewFile` to allocate over 10GB and return no error, which is a fatal OOM rather than a recoverable panic. Which sections get read is not up to the caller. `elf.NewFile` always reads the section-name string table, and `File.Symbols` reads `.symtab` plus whatever section its `Link` field points at, so being selective about sections is not enough to avoid it. New `elfutil.NewFile` is a drop-in for `elf.NewFile` that rejects a declared decompressed size over 128MB. Every production call site goes through it, and a ruleguard rule keeps the next one from going direct. The check runs in two parts, since `debug/elf` expands sections at two different times. The section-name string table is the only one `elf.NewFile` expands itself, so it is checked against the raw bytes before the call; everything else is expanded lazily by `(*Section).Open` and is checked after the parse, where names, types and decompressed sizes are already resolved. Only the sections syft can actually reach are bounded, which keeps the guard from costing real binaries. DWARF is excluded since nothing calls `File.DWARF`, so a large compressed `.debug_info` no longer skips the whole file, and sections `debug/elf` will not decompress anyway (`SHF_ALLOC`, `SHT_NOBITS`) are left alone. The legacy `.zdebug` form is matched on the section name the way `debug/elf` gates it rather than on the `ZLIB` magic, so an ordinary section starting with those four bytes is not mistaken for a compressed one. Signed-off-by: Alex Goodman * fix(elf): gate debug/buildinfo behind the compressed-section check `debug/buildinfo.Read` opens ELF files with `debug/elf` itself, and `elf.NewFile` expands the section-name string table as it parses, so the golang cataloger was still reachable by the same bomb `elfutil` exists to stop. A 261KB fixture drove 1.4GB of allocation through `buildinfo.Read` and returned no error. `elfutil.CheckSectionNameTable` is now exported for that case: callers that cannot use `NewFile` because the `debug/elf` call is made for them inside another package. Both `buildinfo.Read` call sites go through it, including the UPX-decompressed one. Also corrects claims that did not hold up: - the package doc's 2MB-to-10GB figure is not reachable with zlib (~1000:1), so it now carries the measured 510KB-to-2.6GB, and names zstd's 32767:1 since that is what makes the small inputs possible - `.go.buildinfo` was listed as a hot-path section elfutil covers, but it is read through `debug/buildinfo` and never touches `Section.Data` - the graalvm comment claimed routing size rejections away from `*elf.FormatError` improved reporting; both branches are skipped by the caller and only the FormatError branch logs, so it did the opposite - `sharedLibraries` logged short and truncated files as real ELF failures, since `debug/elf` returns a bare `io.EOF` rather than an `*elf.FormatError` for those Signed-off-by: Alex Goodman * added test comments around the negative cases Signed-off-by: Alex Goodman * additional tests Signed-off-by: Alex Goodman * better decomposition and comments Signed-off-by: Alex Goodman * use a less brittle constant for error detection Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- syft/file/cataloger/executable/elf.go | 3 +- syft/internal/elfutil/elfutil.go | 422 ++++++++++++ syft/internal/elfutil/elfutil_test.go | 652 ++++++++++++++++++ .../cataloger/binary/elf_package_cataloger.go | 4 +- .../internal/cli/commands/write_snippet.go | 3 +- syft/pkg/cataloger/golang/parse_go_binary.go | 4 +- syft/pkg/cataloger/golang/scan_binary.go | 16 +- .../cataloger/golang/scan_binary_bomb_test.go | 101 +++ syft/pkg/cataloger/golang/symbols.go | 5 +- .../cataloger/internal/binutils/classifier.go | 14 +- .../internal/dotnet/bundle/bundle_elf.go | 3 +- .../java/graalvm_native_image_cataloger.go | 4 +- .../kernel/parse_linux_kernel_module_file.go | 4 +- test/rules/rules.go | 13 + 14 files changed, 1233 insertions(+), 15 deletions(-) create mode 100644 syft/internal/elfutil/elfutil.go create mode 100644 syft/internal/elfutil/elfutil_test.go create mode 100644 syft/pkg/cataloger/golang/scan_binary_bomb_test.go diff --git a/syft/file/cataloger/executable/elf.go b/syft/file/cataloger/executable/elf.go index b9d2205cf..e7f6d588d 100644 --- a/syft/file/cataloger/executable/elf.go +++ b/syft/file/cataloger/executable/elf.go @@ -10,11 +10,12 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" ) func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error { - f, err := elf.NewFile(reader) + f, err := elfutil.NewFile(reader) if err != nil { return err } diff --git a/syft/internal/elfutil/elfutil.go b/syft/internal/elfutil/elfutil.go new file mode 100644 index 000000000..4870466f3 --- /dev/null +++ b/syft/internal/elfutil/elfutil.go @@ -0,0 +1,422 @@ +/* +Package elfutil provides an ELF opener that rejects sections whose compression headers declare an +implausible decompressed size. + +debug/elf sizes a section's buffer from the section's own compression header, and a highly compressible +stream really does deliver the bytes that header promises, so internal/saferio's chunked read does not +help: it appends its way to every declared byte, and Go's 1.25x slice growth puts the lifetime cost near +five times that. 512MB of zeros is 510KB of zlib, and that file drives elf.NewFile through 2.6GB of +allocation and returns no error, which is a fatal Go OOM rather than a recoverable panic. Zlib caps out +near 1000:1, but debug/elf also accepts zstd, which passes 32000:1 on zeroed input, so the input needed +to declare a given size is smaller still. + +The check runs in two parts, because debug/elf expands sections at two different times: + + - the section-name string table is the one section elf.NewFile expands on its own, so it has to be + bounded against the raw bytes before the call is made. That is CheckSectionNameTable, and the walk + of the section table it needs is why this package decodes any ELF structures by hand at all. + - every other section is expanded lazily by (*Section).Open, so those are bounded after the parse, + where names, types and decompressed sizes are resolved already. + +Only the sections syft can actually drive debug/elf into decompressing are bounded. DWARF is deliberately +excluded: syft never asks for it, so a binary carrying a large compressed .debug_info is cataloged rather +than skipped over a section nobody reads. + +Sources: + - https://www.sco.com/developers/gabi/latest/ch4.sheader.html + - https://groups.google.com/g/generic-abi/c/satyPkuMisk + - https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/linkers-libraries/gnu-style-section-compression.html + - https://sourceware.org/pipermail/binutils/2015-July/089559.html + - https://cs.opensource.google/go/go/+/refs/tags/go1.26.5:src/debug/elf/file.go + - https://go.dev/doc/go1.19#linker +*/ +package elfutil + +import ( + "debug/elf" + "encoding/binary" + "fmt" + "io" + "math" + "strings" + + intFile "github.com/anchore/syft/internal/file" +) + +// maxDeclaredSectionSize bounds the decompressed size any single reachable section may declare. +// +// The bound only ever applies to a compressed section, and the gABI allows compression only on +// non-allocable ones, so nothing syft reads on the hot path (.data, .text, notes) can reach it. The +// realistic worst case is a compressed .symtab/.strtab on a very large binary, well under this. The +// trade-off is that a binary whose symbol or string table decompresses past this is skipped rather than +// cataloged; that is the correct direction to fail, since the alternative is OOM-killing the whole scan. +// +// Note that this is a per-section bound, not a per-file one: a file may hold several sections that each +// sit just under it. That is deliberate, since the sections syft actually reads are few. +const maxDeclaredSectionSize uint64 = 128 * intFile.MB + +// legacyZlibHeaderSize is the size of the .zdebug header: the "ZLIB" magic plus a big-endian size. +const legacyZlibHeaderSize = 12 + +// sectionsReadByName are the sections syft asks debug/elf for by name. Everything else it reaches by +// section type, which reachableSections covers separately. Not every one of these ends in a read of the +// contents (.text is used for its address, .symtab only for a nil check), but bounding any name syft asks +// for is cheaper to keep true than tracking which lookups reach Data. +// +// Add to this list when a cataloger starts reading a new section by name. The ruleguard rule only stops a +// new debug/elf call site; it cannot see a new (*Section).Data call behind an already-bounded open. +var sectionsReadByName = map[string]struct{}{ + ".symtab": {}, + ".data": {}, + ".text": {}, + ".gopclntab": {}, + ".note.package": {}, + ".modinfo": {}, +} + +// wire sizes of the structures we decode, which differ between the two ELF classes +var ( + sizeSection32 = int64(binary.Size(elf.Section32{})) + sizeSection64 = int64(binary.Size(elf.Section64{})) + sizeChdr32 = int64(binary.Size(elf.Chdr32{})) + sizeChdr64 = int64(binary.Size(elf.Chdr64{})) +) + +// NewFile parses an ELF file, rejecting it if a section syft may read declares a decompressed size this +// process should not be asked to allocate. It is a drop-in for elf.NewFile, except that a rejection is +// reported as a plain error rather than an *elf.FormatError, so callers that distinguish "not an ELF" +// from "bad ELF" treat it as the latter. +func NewFile(r io.ReaderAt) (*elf.File, error) { + if err := CheckSectionNameTable(r); err != nil { + return nil, err + } + + f, err := elf.NewFile(r) + if err != nil { + return nil, err + } + + if err := checkReachableSections(f); err != nil { + return nil, err + } + return f, nil +} + +// checkReachableSections bounds every section syft can drive debug/elf into decompressing. This runs +// after the parse because (*Section).Open expands lazily, so nothing has been allocated yet. +func checkReachableSections(f *elf.File) error { + for i := range reachableSections(f) { + s := f.Sections[i] + declared, claimed := declaredSectionSize(s) + if claimed && declared > maxDeclaredSectionSize { + return fmt.Errorf("elf section %q declares %d decompressed bytes, over the %d byte limit", + s.Name, declared, maxDeclaredSectionSize) + } + } + return nil +} + +// reachableSections reports which sections syft can actually reach. It marks by type generously, since +// the debug/elf accessors all go through SectionByType, which returns only the first section of a type: a +// second SHT_SYMTAB is bounded here despite being unreachable. DWARF is absent by design: nothing in syft +// calls File.DWARF, so debug/elf is never asked to expand a .debug_* section. +func reachableSections(f *elf.File) map[int]struct{} { + reach := make(map[int]struct{}) + mark := func(i int) { + if i >= 0 && i < len(f.Sections) { + reach[i] = struct{}{} + } + } + for i, s := range f.Sections { + switch s.Type { + case elf.SHT_SYMTAB, elf.SHT_DYNSYM, elf.SHT_DYNAMIC: + // File.Symbols, File.DynamicSymbols and File.DynString each read one of these plus the + // string table its Link field points at + mark(i) + mark(int(s.Link)) + case elf.SHT_GNU_VERSYM, elf.SHT_GNU_VERDEF, elf.SHT_GNU_VERNEED: + // File.DynamicSymbols pulls the symbol version tables in behind the caller's back, via + // gnuVersionInit. Strictly they are only reachable alongside a .dynsym, and VERDEF/VERNEED + // only alongside a VERSYM, but they are marked unconditionally rather than tracked: the only + // file that loses out carries an oversized version table and no dynamic symbols at all, + // which is not something a toolchain emits. + mark(i) + } + if _, ok := sectionsReadByName[s.Name]; ok { + mark(i) + } + } + return reach +} + +// declaredSectionSize reports the decompressed size a section claims, if it claims one at all. +// +// ELF grew two separate ways of saying "this section is compressed", and (*Section).Open honors both, so +// this dispatches the same way Open does and in the same order. That correspondence is the whole design: +// wherever Open returns a decompressing reader, this reports the size that reader will expand to. Missing +// one is the bug this package exists to prevent, so when a Go release changes Open, this is what has to be +// re-read against it. +// +// It deliberately over-reports in one place, an unrecognized ch_type: Open hands back an error reader for +// anything but zlib and zstd, so nothing is allocated, but the type is not exposed on elf.Section and +// re-reading the header for it would only spare a file debug/elf cannot decompress anyway. +func declaredSectionSize(s *elf.Section) (uint64, bool) { + // SHT_NOBITS marks a section that occupies no bytes in the file, .bss being the familiar one: it + // declares a size the loader zero-fills at run time rather than a size stored anywhere. Its sh_offset + // is only a conceptual placement and routinely lands inside some other section's bytes. Two things + // follow, and both say to test this before looking at compression at all: + // - Open tests it first too, ahead of any compression, and hands back a reader that fails every + // read. Nothing is allocated for the declared size, however large it is. + // - the gABI forbids SHF_COMPRESSED here, but elf.NewFile does not enforce that and parses a + // compression header anyway. The Size it leaves on the section was decoded from whatever bytes lie + // at that borrowed offset, so it is not a figure to bound anything against. + if s.Type == elf.SHT_NOBITS { + return 0, false + } + + if s.Flags&elf.SHF_COMPRESSED != 0 { + return chdrDeclaredSize(s) + } + return zdebugDeclaredSize(s) +} + +// chdrDeclaredSize reports the size declared by the modern compression form: an SHF_COMPRESSED flag, +// plus an Elf32_Chdr or Elf64_Chdr at the head of the section giving the algorithm and the decompressed +// size. This is the form standardized in the gABI and the one any current toolchain emits. +func chdrDeclaredSize(s *elf.Section) (uint64, bool) { + // SHF_ALLOC marks a section the loader maps into the running process, and nothing decompresses a + // section on its way into memory, so the gABI makes the two flags mutually exclusive. Open enforces + // that rather than tolerating it: a section setting both gets an error reader, never a decompressing + // one, so a size declared alongside SHF_ALLOC is never allocated. + if s.Flags&elf.SHF_ALLOC != 0 { + return 0, false + } + // elf.NewFile decodes the Chdr during the parse and overwrites Size with ch_size, leaving the on-disk + // figure in FileSize. So the number a hostile file controls is already here, and no second read of the + // file is needed. + return s.Size, true +} + +// zdebugDeclaredSize reports the size declared by the legacy compression form. Before SHF_COMPRESSED was +// added to the gABI, GNU tooling signaled a compressed debug section by renaming it, .debug_info to +// .zdebug_info, and prefixing the payload with a header of its own: the magic "ZLIB" followed by the +// decompressed size. Toolchains default to the gABI form now, Go's own linker having emitted .zdebug up +// to 1.19, but this is not vestigial: nothing ties the prefix to a debug section, so a crafted file can +// hang it on a section syft does read, an SHT_SYMTAB named .zdebug_x being the obvious one. +func zdebugDeclaredSize(s *elf.Section) (uint64, bool) { + // no flag distinguishes this form, so the section name is the only signal, and Open gates on the name + // before it so much as looks at the bytes. Matching that gate exactly is what keeps an ordinary + // section whose contents happen to begin with "ZLIB" out of scope: Open will never decompress it, so + // neither may this. + if !strings.HasPrefix(s.Name, ".zdebug") { + return 0, false + } + // unlike the Chdr above, nothing in the parse has looked at this header, so read it here. Reading off + // the section is only safe because SHF_COMPRESSED is clear on this path, since elf.NewFile leaves the + // embedded ReaderAt nil for a compressed one. Open treats a short read or absent magic as "not really + // compressed after all" and serves the bytes as they lie. + var hdr [legacyZlibHeaderSize]byte + if n, _ := s.ReadAt(hdr[:], 0); n != legacyZlibHeaderSize || string(hdr[:4]) != "ZLIB" { + return 0, false + } + // the size is big-endian whatever byte order the rest of the file uses. The header predates the gABI + // mechanism and was simply defined that way, carrying no endianness of its own. + return binary.BigEndian.Uint64(hdr[4:]), true +} + +// CheckSectionNameTable bounds the one section elf.NewFile expands on its own. Anything it cannot walk is +// passed through untouched, leaving elf.NewFile to describe the file. It does not repeat every check +// elf.NewFile makes ahead of that expansion, though, so a file that is both malformed and oversized can +// come back with this error rather than an *elf.FormatError. +// +// NewFile calls this itself. It is exported for the callers that cannot use NewFile because the +// debug/elf call is made for them inside another package, debug/buildinfo being the one syft reaches: +// gate the reader on this and the eager section-name table read is bounded, which is everything that +// package expands today, since it reaches .go.buildinfo through the program headers instead. +func CheckSectionNameTable(r io.ReaderAt) error { + class, order, ok := identify(r) + if !ok { + return nil + } + sh, ok := sectionNameTableHeader(r, class, order) + if !ok { + return nil + } + // the name table's own name is not resolved at the point elf.NewFile reads it, so debug/elf cannot + // take the legacy .zdebug path for this section: only a compression header can apply + if sh.flags&uint64(elf.SHF_COMPRESSED) == 0 { + return nil + } + declared, err := compressedSize(r, class, order, sh.offset) + if err != nil { + return nil //nolint:nilerr // truncated section; elf.NewFile will say so + } + if declared > maxDeclaredSectionSize { + return fmt.Errorf("elf section name table declares %d decompressed bytes, over the %d byte limit", + declared, maxDeclaredSectionSize) + } + return nil +} + +// identify decodes the class and byte order every later read depends on, out of the file's 16 +// identification bytes. It reports false for anything that is not an ELF this package understands, so +// elf.NewFile remains the single source of format errors. +func identify(r io.ReaderAt) (elf.Class, binary.ByteOrder, bool) { + var ident [16]byte + if _, err := io.ReadFull(io.NewSectionReader(r, 0, int64(len(ident))), ident[:]); err != nil { + return 0, nil, false + } + if string(ident[:4]) != elf.ELFMAG { + return 0, nil, false + } + + class := elf.Class(ident[elf.EI_CLASS]) + if class != elf.ELFCLASS32 && class != elf.ELFCLASS64 { + return 0, nil, false + } + + switch elf.Data(ident[elf.EI_DATA]) { + case elf.ELFDATA2LSB: + return class, binary.LittleEndian, true + case elf.ELFDATA2MSB: + return class, binary.BigEndian, true + } + return 0, nil, false +} + +// sectionNameTableHeader locates the header of the section syft's own parse cannot reach, the one +// elf.NewFile expands for itself. It reports false whenever the file names no name table or is malformed +// enough that nothing gets decompressed: either way there is nothing to bound, and describing the file is +// elf.NewFile's job rather than this package's. +func sectionNameTableHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder) (sectionHeader, bool) { + shoff, shentsize, shnum, shstrndx, err := fileHeader(r, class, order) + if err != nil || shoff <= 0 || shentsize < sectionHeaderSize(class) { + return sectionHeader{}, false + } + + // a zero e_shnum means the real count lives in section 0's size field, and an e_shstrndx of + // SHN_XINDEX means the real string table index lives in that same header's link field + if shnum == 0 { + sh, err := readSectionHeader(r, class, order, shoff) + if err != nil { + return sectionHeader{}, false + } + shnum = sh.size + if shstrndx == uint64(elf.SHN_XINDEX) { + shstrndx = uint64(sh.link) + } + } + + // an index of zero means the file has no section name table, so elf.NewFile returns before reading one. + // An out-of-range index expands nothing either, though elf.NewFile only rejects it when e_shnum was + // non-zero; reached through SHN_XINDEX it indexes out of range and panics instead. That is a + // recoverable panic the task executor already contains, unlike the OOM this package exists to stop. + if shstrndx == 0 || shstrndx >= shnum { + return sectionHeader{}, false + } + // the index is file-controlled, so guard the multiply and then the sum against a table the file + // places past the end of the address space + if shstrndx > uint64(math.MaxInt64)/uint64(shentsize) { + return sectionHeader{}, false + } + off := shoff + int64(shstrndx)*shentsize + if off < 0 { + return sectionHeader{}, false + } + + sh, err := readSectionHeader(r, class, order, off) + if err != nil { + return sectionHeader{}, false + } + return sh, true +} + +// sectionHeader is the handful of fields we need out of an Elf32_Shdr or an Elf64_Shdr. +type sectionHeader struct { + flags uint64 + offset int64 + size uint64 + link uint32 +} + +// fileHeader returns the section table location and shape out of the ELF file header. +func fileHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder) (shoff, shentsize int64, shnum, shstrndx uint64, err error) { + if class == elf.ELFCLASS32 { + var h elf.Header32 + if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(h))), order, &h); err != nil { + return 0, 0, 0, 0, err + } + return int64(h.Shoff), int64(h.Shentsize), uint64(h.Shnum), uint64(h.Shstrndx), nil + } + var h elf.Header64 + if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(h))), order, &h); err != nil { + return 0, 0, 0, 0, err + } + // Shoff is a uint64, so a file may claim an offset that does not fit in an int64; the caller rejects + // a negative result rather than wrapping into a plausible-looking one + return int64(h.Shoff), int64(h.Shentsize), uint64(h.Shnum), uint64(h.Shstrndx), nil +} + +// readSectionHeader decodes one section header. Reads go through binary.Read, which uses io.ReadFull, so +// a short read is an error rather than a partially-zeroed header. +func readSectionHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder, off int64) (sectionHeader, error) { + sr := io.NewSectionReader(r, off, sectionHeaderSize(class)) + if class == elf.ELFCLASS32 { + var sh elf.Section32 + if err := binary.Read(sr, order, &sh); err != nil { + return sectionHeader{}, err + } + return sectionHeader{ + flags: uint64(sh.Flags), + offset: int64(sh.Off), + size: uint64(sh.Size), + link: sh.Link, + }, nil + } + var sh elf.Section64 + if err := binary.Read(sr, order, &sh); err != nil { + return sectionHeader{}, err + } + return sectionHeader{ + flags: sh.Flags, + offset: int64(sh.Off), + size: sh.Size, + link: sh.Link, + }, nil +} + +// compressedSize reads the decompressed size out of a section's SHF_COMPRESSED header. Chdr64 carries a +// blank field that binary.Size counts and binary.Read skips, so the decoded offsets match what debug/elf +// reaches for with unsafe.Offsetof. +func compressedSize(r io.ReaderAt, class elf.Class, order binary.ByteOrder, off int64) (uint64, error) { + if off < 0 { + return 0, fmt.Errorf("negative section offset") + } + sr := io.NewSectionReader(r, off, compressionHeaderSize(class)) + if class == elf.ELFCLASS32 { + var ch elf.Chdr32 + if err := binary.Read(sr, order, &ch); err != nil { + return 0, err + } + return uint64(ch.Size), nil + } + var ch elf.Chdr64 + if err := binary.Read(sr, order, &ch); err != nil { + return 0, err + } + return ch.Size, nil +} + +func sectionHeaderSize(class elf.Class) int64 { + if class == elf.ELFCLASS32 { + return sizeSection32 + } + return sizeSection64 +} + +func compressionHeaderSize(class elf.Class) int64 { + if class == elf.ELFCLASS32 { + return sizeChdr32 + } + return sizeChdr64 +} diff --git a/syft/internal/elfutil/elfutil_test.go b/syft/internal/elfutil/elfutil_test.go new file mode 100644 index 000000000..3d70b69f0 --- /dev/null +++ b/syft/internal/elfutil/elfutil_test.go @@ -0,0 +1,652 @@ +package elfutil + +import ( + "bytes" + "compress/zlib" + "debug/elf" + "encoding/binary" + "fmt" + "io" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// section describes one section to place in a fixture. The compression headers are deliberately +// separable from the payload so a fixture can claim far more than it can deliver, which is the whole +// shape of the attack under test. +type section struct { + name string + typ elf.SectionType + flags elf.SectionFlag + link uint32 + body []byte + compressed bool // emit a SHF_COMPRESSED chdr ahead of body + legacyZlib bool // emit a 12-byte .zdebug-style ZLIB header ahead of body + declaredSize uint64 + offsetOf string // borrow another section's file offset, for SHT_NOBITS fixtures + shSize uint64 // override sh_size, since an SHT_NOBITS section's size is not its span in the file +} + +type buildOpts struct { + // nameTable overrides the generated .shstrtab, so a fixture can compress the one section + // elf.NewFile expands on its own. + nameTable *section + // extendedShnum parks the real section count in section 0 and zeroes e_shnum. + extendedShnum bool + // xindexShstrndx sets e_shstrndx to SHN_XINDEX so the real index comes from section 0's link. + xindexShstrndx bool +} + +// buildELF assembles a minimal ELF: a null section, then secs, then a generated .shstrtab that +// e_shstrndx points at. +func buildELF(t *testing.T, class elf.Class, order binary.ByteOrder, secs []section, opts buildOpts) []byte { + t.Helper() + + nameTable := section{name: ".shstrtab", typ: elf.SHT_STRTAB} + if opts.nameTable != nil { + nameTable = *opts.nameTable + nameTable.name = ".shstrtab" + nameTable.typ = elf.SHT_STRTAB + } + + all := append([]section{{}}, secs...) + all = append(all, nameTable) + shstrndx := len(all) - 1 + + // the name table's body is the names of every section, so it has to be built before the payloads + var names bytes.Buffer + names.WriteByte(0) + nameOff := make([]uint32, len(all)) + for i, s := range all { + if s.name == "" { + continue + } + nameOff[i] = uint32(names.Len()) + names.WriteString(s.name) + names.WriteByte(0) + } + all[shstrndx].body = names.Bytes() + // a fixture that compresses the name table without rigging a size gets an honest one, so the file is + // readable rather than merely parseable + if all[shstrndx].compressed && all[shstrndx].declaredSize == 0 { + all[shstrndx].declaredSize = uint64(names.Len()) + all[shstrndx].body = deflate(t, names.Bytes()) + } + + is32 := class == elf.ELFCLASS32 + ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{}) + machine := elf.EM_X86_64 + if is32 { + ehsize, shentsize = binary.Size(elf.Header32{}), binary.Size(elf.Section32{}) + machine = elf.EM_386 + } + + shoff := ehsize + dataOff := shoff + len(all)*shentsize + + // lay the payloads out and remember where each landed, so an SHT_NOBITS fixture can point at one + payloads := make([][]byte, len(all)) + offsets := make(map[string]uint64) + cursor := uint64(dataOff) + for i, s := range all { + payloads[i] = sectionContent(t, class, order, s) + offsets[s.name] = cursor + cursor += uint64(len(payloads[i])) + } + + var ident [16]byte + copy(ident[:], elf.ELFMAG) + ident[elf.EI_CLASS] = byte(class) + ident[elf.EI_DATA] = byte(elf.ELFDATA2LSB) + if order == binary.BigEndian { + ident[elf.EI_DATA] = byte(elf.ELFDATA2MSB) + } + ident[elf.EI_VERSION] = byte(elf.EV_CURRENT) + + shnum := uint16(len(all)) + if opts.extendedShnum { + shnum = 0 + } + strndx := uint16(shstrndx) + if opts.xindexShstrndx { + strndx = uint16(elf.SHN_XINDEX) + } + + buf := &bytes.Buffer{} + if is32 { + write(t, buf, order, elf.Header32{ + Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(machine), Version: uint32(elf.EV_CURRENT), + Shoff: uint32(shoff), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), + Shnum: shnum, Shstrndx: strndx, + }) + } else { + write(t, buf, order, elf.Header64{ + Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(machine), Version: uint32(elf.EV_CURRENT), + Shoff: uint64(shoff), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), + Shnum: shnum, Shstrndx: strndx, + }) + } + + off := uint64(dataOff) + for i, s := range all { + size := uint64(len(payloads[i])) + if s.shSize != 0 { + size = s.shSize + } + secOff := off + if s.offsetOf != "" { + secOff = offsets[s.offsetOf] + } + flags := s.flags + if s.compressed { + // the content header and the flag always travel together in a real file + flags |= elf.SHF_COMPRESSED + } + link := s.link + // section 0 carries the extended count and the extended name table index + if i == 0 { + if opts.extendedShnum { + size = uint64(len(all)) + } + if opts.xindexShstrndx { + link = uint32(shstrndx) + } + } + if is32 { + write(t, buf, order, elf.Section32{ + Name: nameOff[i], Type: uint32(s.typ), Flags: uint32(flags), Link: link, + Off: uint32(secOff), Size: uint32(size), Addralign: 1, + }) + } else { + write(t, buf, order, elf.Section64{ + Name: nameOff[i], Type: uint32(s.typ), Flags: uint64(flags), Link: link, + Off: secOff, Size: size, Addralign: 1, + }) + } + off += uint64(len(payloads[i])) + } + + require.Equal(t, dataOff, buf.Len(), "fixture layout drifted from the declared offsets") + for _, p := range payloads { + buf.Write(p) + } + return buf.Bytes() +} + +// sectionContent wraps the body in whichever compression header the fixture asks for. +func sectionContent(t *testing.T, class elf.Class, order binary.ByteOrder, s section) []byte { + t.Helper() + + switch { + case s.legacyZlib: + hdr := make([]byte, legacyZlibHeaderSize) + copy(hdr, "ZLIB") + binary.BigEndian.PutUint64(hdr[4:], s.declaredSize) + return append(hdr, s.body...) + case s.compressed: + buf := &bytes.Buffer{} + if class == elf.ELFCLASS32 { + write(t, buf, order, elf.Chdr32{ + Type: uint32(elf.COMPRESS_ZLIB), Size: uint32(s.declaredSize), Addralign: 1, + }) + } else { + write(t, buf, order, elf.Chdr64{ + Type: uint32(elf.COMPRESS_ZLIB), Size: s.declaredSize, Addralign: 1, + }) + } + buf.Write(s.body) + return buf.Bytes() + default: + return s.body + } +} + +func write(t *testing.T, w io.Writer, order binary.ByteOrder, v any) { + t.Helper() + require.NoError(t, binary.Write(w, order, v)) +} + +// deflate returns a real zlib stream, so a fixture using it actually delivers the bytes it promises. +func deflate(t *testing.T, payload []byte) []byte { + t.Helper() + buf := &bytes.Buffer{} + zw := zlib.NewWriter(buf) + _, err := zw.Write(payload) + require.NoError(t, err) + require.NoError(t, zw.Close()) + return buf.Bytes() +} + +// classes is the matrix every structural case runs under, since the header layouts differ per class and +// the compression header's size field is a different width in each. +var classes = []struct { + name string + class elf.Class + order binary.ByteOrder +}{ + {"64-bit little-endian", elf.ELFCLASS64, binary.LittleEndian}, + {"64-bit big-endian", elf.ELFCLASS64, binary.BigEndian}, + {"32-bit little-endian", elf.ELFCLASS32, binary.LittleEndian}, + {"32-bit big-endian", elf.ELFCLASS32, binary.BigEndian}, +} + +const overLimit = maxDeclaredSectionSize + 1 + +func TestNewFile_RejectsOversizedReachableSections(t *testing.T) { + tests := []struct { + name string + secs []section + opts buildOpts + wantErr string + }{ + { + name: "compressed .symtab", + secs: []section{ + {name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, compressed: true, declaredSize: overLimit}, + {name: ".strtab", typ: elf.SHT_STRTAB}, + }, + wantErr: `".symtab"`, + }, + { + name: "the string table a .symtab links to", + secs: []section{ + {name: ".symtab", typ: elf.SHT_SYMTAB, link: 2}, + {name: ".strtab", typ: elf.SHT_STRTAB, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".strtab"`, + }, + { + name: "compressed .dynsym", + secs: []section{ + {name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2, compressed: true, declaredSize: overLimit}, + {name: ".dynstr", typ: elf.SHT_STRTAB}, + }, + wantErr: `".dynsym"`, + }, + { + name: "compressed .dynamic", + secs: []section{ + {name: ".dynamic", typ: elf.SHT_DYNAMIC, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".dynamic"`, + }, + { + name: "compressed .note.package, reached by name rather than type", + secs: []section{ + {name: ".note.package", typ: elf.SHT_NOTE, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".note.package"`, + }, + { + name: "compressed .modinfo, read by the kernel module cataloger", + secs: []section{ + {name: ".modinfo", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".modinfo"`, + }, + { + // File.DynamicSymbols reads these via gnuVersionInit without the caller naming them + name: "compressed .gnu.version alongside a .dynsym", + secs: []section{ + {name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2}, + {name: ".dynstr", typ: elf.SHT_STRTAB}, + {name: ".gnu.version", typ: elf.SHT_GNU_VERSYM, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".gnu.version"`, + }, + { + name: "compressed .gnu.version_r alongside a .dynsym", + secs: []section{ + {name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2}, + {name: ".dynstr", typ: elf.SHT_STRTAB}, + {name: ".gnu.version_r", typ: elf.SHT_GNU_VERNEED, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".gnu.version_r"`, + }, + { + name: "compressed .gnu.version_d alongside a .dynsym", + secs: []section{ + {name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2}, + {name: ".dynstr", typ: elf.SHT_STRTAB}, + {name: ".gnu.version_d", typ: elf.SHT_GNU_VERDEF, compressed: true, declaredSize: overLimit}, + }, + wantErr: `".gnu.version_d"`, + }, + { + // the one section elf.NewFile expands itself, so this has to be caught before the parse + name: "compressed section name table", + opts: buildOpts{nameTable: §ion{compressed: true, declaredSize: overLimit}}, + wantErr: "section name table", + }, + { + // debug/elf gates the legacy form on the name, so a reachable section named this way reaches it + name: "legacy .zdebug header on a section reached by type", + secs: []section{ + {name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2, legacyZlib: true, declaredSize: overLimit}, + {name: ".strtab", typ: elf.SHT_STRTAB}, + }, + wantErr: `".zdebug_symtab"`, + }, + } + + for _, tt := range tests { + for _, c := range classes { + t.Run(tt.name+"/"+c.name, func(t *testing.T) { + data := buildELF(t, c.class, c.order, tt.secs, tt.opts) + _, err := NewFile(bytes.NewReader(data)) + require.Error(t, err) + // the fixtures are hand-assembled ELF bytes, so a bad one would satisfy require.Error on + // its own. Naming the section proves the rejection is the one we set up, and naming the + // limit proves it came from this package rather than from debug/elf. + assert.Contains(t, err.Error(), tt.wantErr) + assert.Contains(t, err.Error(), fmt.Sprint(maxDeclaredSectionSize)) + }) + } + } +} + +// TestNewFile_AcceptsWhatDebugELFWouldNotExpand covers the false-negative direction: rejecting a whole +// binary over a section debug/elf is never driven to decompress drops real packages from the SBOM. +// +// Every fixture here is a file debug/elf parses without complaint, and every one must come back from +// NewFile without complaint too. An oversized claim is only worth rejecting when debug/elf can actually +// be made to allocate against it, so a claim it will never act on has to be ignored outright rather than +// merely tolerated. +func TestNewFile_AcceptsWhatDebugELFWouldNotExpand(t *testing.T) { + tests := []struct { + name string + secs []section + why string + }{ + { + // only File.DWARF expands a .debug_* section, and nothing in syft calls it, so this claim is + // never acted on. reachableSections leaves DWARF out on purpose: compressed debug info is + // routinely enormous in a legitimate binary, and bounding it would skip the binary entirely. + // + // non-allocable and SHF_COMPRESSED is deliberately the one shape debug/elf will expand, so the + // name is the only thing keeping this accepted. Adding SHF_ALLOC would make it pass for the + // allocable case's reason below and stop saying anything about DWARF. + name: "oversized compressed DWARF", + secs: []section{ + {name: ".debug_info", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit}, + }, + why: "syft never calls File.DWARF, so debug/elf is never asked to expand it", + }, + { + // the same section in the older form toolchains emitted before SHF_COMPRESSED existed. The + // header is different, the reader that would expand it is the same one syft never calls. + // + // SHF_COMPRESSED is deliberately absent: with it set, debug/elf takes the modern path and the + // legacy header is never consulted, so the .zdebug name gate would go untested. + name: "oversized legacy .zdebug DWARF", + secs: []section{ + {name: ".zdebug_info", typ: elf.SHT_PROGBITS, legacyZlib: true, declaredSize: overLimit}, + }, + why: "same, via the legacy form", + }, + { + // reachable by type and named the way debug/elf gates the legacy form on, so the name half of + // that gate passes and the magic is the only thing left to reject the claim. Without the magic + // check the 0xff bytes behind it decode as a declared size of 2^64-1. + name: "a .zdebug-named reachable section whose bytes are not a legacy header", + secs: []section{ + {name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2, + body: append([]byte("NOPE"), bytes.Repeat([]byte{0xff}, 32)...)}, + {name: ".strtab", typ: elf.SHT_STRTAB}, + }, + why: "the legacy form needs the magic as well as the name", + }, + { + // the other half of the same gate: fewer bytes present than a legacy header needs. The magic is + // intact and some of the size field is present, so a short read has to read as "no claim" + // rather than as a size decoded out of a partly-filled buffer, which here would be huge. + name: "a .zdebug-named reachable section too short to hold a legacy header", + secs: []section{ + {name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2, + body: append([]byte("ZLIB"), bytes.Repeat([]byte{0xff}, 4)...)}, + {name: ".strtab", typ: elf.SHT_STRTAB}, + }, + why: "a truncated legacy header is not a claim", + }, + { + // an ordinary uncompressed note that happens to open with those four bytes. debug/elf takes + // the legacy path only for a .zdebug-prefixed name, so it reads this as content and nothing + // here is a size claim at all. A check that sniffed for the magic instead would decode the + // 0xff padding behind it as a declared size and throw the binary away. + // + // .note.package because it has to be a section syft actually reads, or reachableSections skips + // it and the case passes without exercising anything. No flags for the same reason the name is + // not .zdebug-prefixed: either one would put debug/elf on a path where the bytes are a header. + name: "an uncompressed section whose bytes happen to start with ZLIB", + secs: []section{ + {name: ".note.package", typ: elf.SHT_NOTE, body: append([]byte("ZLIB"), bytes.Repeat([]byte{0xff}, 32)...)}, + }, + why: "debug/elf gates the legacy form on the section name, not the magic, so this is not a claim at all", + }, + { + // a section that occupies memory at runtime cannot be compressed on disk, so (*Section).Open + // hands back the raw bytes rather than expanding one that says it is. The claim is real and + // oversized, and still costs nothing. + // + // this is the ".symtab" reject case with one bit added: .data is read by name just as .symtab + // is, so SHF_ALLOC is the only reason the two outcomes differ. + name: "an allocable compressed section", + secs: []section{ + {name: ".data", typ: elf.SHT_PROGBITS, flags: elf.SHF_COMPRESSED | elf.SHF_ALLOC, + compressed: true, declaredSize: overLimit}, + }, + why: "debug/elf refuses to decompress an allocable section", + }, + { + // the hardest one to see: sh_offset points into .rodata's payload, so elf.NewFile reads that + // compression header and .data comes back carrying an oversized Size of its own. Reading Size + // alone would reject here, but a SHT_NOBITS section holds no file bytes and its reader yields + // none, so the size is never allocated against. sh_size is set by hand because such a section + // has a size without occupying any of the file, and a zero one is a file debug/elf rejects. + // + // SHF_COMPRESSED has to be set for the size to be a claim at all, and .data has to be the + // borrower rather than the donor, since the donor is unreachable by name and never checked. + name: "an SHT_NOBITS section borrowing another section's compression header", + secs: []section{ + {name: ".rodata", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit}, + {name: ".data", typ: elf.SHT_NOBITS, flags: elf.SHF_COMPRESSED, offsetOf: ".rodata", + shSize: 4096}, + }, + why: "debug/elf never yields bytes for SHT_NOBITS, whatever Size it ends up carrying", + }, + { + // a string table is reached through some symbol table's sh_link, never on its own, so one + // nothing points at is one nothing reads. This is the reject test's linked-.strtab case with + // the .symtab removed, and it is the case an implementation that bounded every SHT_STRTAB + // rather than following the links would fail. + name: "an oversized string table that no symbol table links", + secs: []section{ + {name: ".strtab", typ: elf.SHT_STRTAB, compressed: true, declaredSize: overLimit}, + }, + why: "reachability runs through sh_link, not section type", + }, + } + + for _, tt := range tests { + for _, c := range classes { + t.Run(tt.name+"/"+c.name, func(t *testing.T) { + data := buildELF(t, c.class, c.order, tt.secs, buildOpts{}) + + // "elfutil accepted it" only means something if debug/elf accepts it too, so a fixture + // that drifts into being malformed fails here rather than passing for the wrong reason + _, err := elf.NewFile(bytes.NewReader(data)) + require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts") + + _, err = NewFile(bytes.NewReader(data)) + require.NoError(t, err, tt.why) + }) + } + } +} + +// TestNewFile_AcceptsAndReadsSectionUnderTheLimit guards the risk the bound introduces: a legitimately +// compressed section must still be readable, not merely accepted. +func TestNewFile_AcceptsAndReadsSectionUnderTheLimit(t *testing.T) { + for _, c := range classes { + t.Run(c.name, func(t *testing.T) { + payload := bytes.Repeat([]byte("syft"), 4096) + data := buildELF(t, c.class, c.order, []section{ + {name: ".note.package", typ: elf.SHT_NOTE, flags: elf.SHF_COMPRESSED, + compressed: true, declaredSize: uint64(len(payload)), body: deflate(t, payload)}, + }, buildOpts{}) + + f, err := NewFile(bytes.NewReader(data)) + require.NoError(t, err) + + got, err := f.Section(".note.package").Data() + require.NoError(t, err) + assert.Equal(t, payload, got) + }) + } +} + +// TestNewFile_AcceptsAndReadsCompressedSectionNameTable covers the other half of CheckSectionNameTable. +// That check runs before the parse, off a hand-decoded section header rather than anything debug/elf has +// resolved, so a version of it that rejected every compressed name table outright, or that read the size +// out of the wrong offset, would still pass every rejection case. Resolving the names proves the file was +// left intact and not merely let through. +func TestNewFile_AcceptsAndReadsCompressedSectionNameTable(t *testing.T) { + for _, c := range classes { + t.Run(c.name, func(t *testing.T) { + data := buildELF(t, c.class, c.order, + []section{{name: ".note.package", typ: elf.SHT_NOTE}}, + buildOpts{nameTable: §ion{compressed: true}}) + + f, err := NewFile(bytes.NewReader(data)) + require.NoError(t, err) + assert.NotNil(t, f.Section(".note.package"), "section names did not survive the name table") + }) + } +} + +// TestNewFile_BombDoesNotAllocate is the end-to-end case: a real zlib stream that genuinely delivers +// every byte its header promises, so without the bound debug/elf allocates all of them. +func TestNewFile_BombDoesNotAllocate(t *testing.T) { + const declared = maxDeclaredSectionSize + 1 + compressed := deflate(t, make([]byte, declared)) + t.Logf("%d declared bytes compress to %d", declared, len(compressed)) + + data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, []section{ + {name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, flags: elf.SHF_COMPRESSED, + compressed: true, declaredSize: declared, body: compressed}, + {name: ".strtab", typ: elf.SHT_STRTAB}, + }, buildOpts{}) + + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + + _, err := NewFile(bytes.NewReader(data)) + + runtime.ReadMemStats(&after) + + require.Error(t, err) + assert.Contains(t, err.Error(), "over the") + + // the guard has to reject before the allocation, not after it. The margin is loose on purpose: this + // is asserting "nothing near the declared size was allocated", not a precise budget. + const margin = 32 * 1024 * 1024 + assert.Less(t, after.TotalAlloc-before.TotalAlloc, uint64(margin), + "parsing allocated far more than the input warrants") +} + +// TestNewFile_MatchesDebugELFOnStructuralEdges pins the invariant the passthrough returns depend on: +// wherever elfutil declines to check, elf.NewFile must still be the one deciding. +func TestNewFile_MatchesDebugELFOnStructuralEdges(t *testing.T) { + tests := []struct { + name string + secs []section + opts buildOpts + }{ + {name: "plain file", secs: []section{{name: ".text", typ: elf.SHT_PROGBITS, flags: elf.SHF_ALLOC}}}, + {name: "extended section count", opts: buildOpts{extendedShnum: true}}, + {name: "extended name table index", opts: buildOpts{xindexShstrndx: true}}, + {name: "both extended forms", opts: buildOpts{extendedShnum: true, xindexShstrndx: true}}, + { + name: "symtab linking a nonexistent string table", + secs: []section{{name: ".symtab", typ: elf.SHT_SYMTAB, link: 99}}, + }, + } + + for _, tt := range tests { + for _, c := range classes { + t.Run(tt.name+"/"+c.name, func(t *testing.T) { + data := buildELF(t, c.class, c.order, tt.secs, tt.opts) + + _, want := elf.NewFile(bytes.NewReader(data)) + _, got := NewFile(bytes.NewReader(data)) + + if want == nil { + assert.NoError(t, got, "elfutil rejected a file debug/elf accepts") + return + } + require.Error(t, got, "elfutil accepted a file debug/elf rejects") + }) + } + } +} + +// TestNewFile_ExtendedShnumOverflowStillChecks covers the case a signed conversion used to swallow: a +// count that does not fit an int64 must not read as "no sections" and skip the check. +func TestNewFile_ExtendedShnumOverflowStillChecks(t *testing.T) { + data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil, + buildOpts{nameTable: §ion{compressed: true, declaredSize: overLimit}}) + + // rewrite section 0's size to a count that overflows int64, and zero e_shnum so it is consulted + // e_shnum sits at 0x3c in an Elf64_Ehdr, and sh_size at +32 in the Elf64_Shdr that follows it + shoff := binary.Size(elf.Header64{}) + binary.LittleEndian.PutUint16(data[0x3c:], 0) + binary.LittleEndian.PutUint64(data[shoff+32:], uint64(1)<<63) + + _, err := NewFile(bytes.NewReader(data)) + require.Error(t, err, "a bogus section count must not disable the check") +} + +func TestNewFile_PassesThroughNonELF(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + {"empty", nil}, + {"shorter than e_ident", []byte{0x7f, 'E'}}, + {"bad magic", bytes.Repeat([]byte{0xab}, 128)}, + {"bad class", func() []byte { + b := bytes.Repeat([]byte{0}, 128) + copy(b, elf.ELFMAG) + b[elf.EI_CLASS] = 9 + return b + }()}, + {"bad byte order", func() []byte { + b := bytes.Repeat([]byte{0}, 128) + copy(b, elf.ELFMAG) + b[elf.EI_CLASS] = byte(elf.ELFCLASS64) + b[elf.EI_DATA] = 9 + return b + }()}, + {"truncated after the header", func() []byte { + data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil, buildOpts{}) + return data[:binary.Size(elf.Header64{})+4] + }()}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, want := elf.NewFile(bytes.NewReader(tt.data)) + _, got := NewFile(bytes.NewReader(tt.data)) + + require.Error(t, want, "fixture is supposed to be rejected by debug/elf") + require.Error(t, got) + // the rejection must be debug/elf's, phrased its way, not ours + assert.NotContains(t, got.Error(), "over the") + assert.Equal(t, fmt.Sprint(want), fmt.Sprint(got)) + }) + } +} diff --git a/syft/pkg/cataloger/binary/elf_package_cataloger.go b/syft/pkg/cataloger/binary/elf_package_cataloger.go index 07cbccbe2..943550d2e 100644 --- a/syft/pkg/cataloger/binary/elf_package_cataloger.go +++ b/syft/pkg/cataloger/binary/elf_package_cataloger.go @@ -3,7 +3,6 @@ package binary import ( "bytes" "context" - "debug/elf" "encoding/binary" "encoding/json" "fmt" @@ -14,6 +13,7 @@ import ( "github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" ) @@ -146,7 +146,7 @@ func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) { return nil, fmt.Errorf("unable to get union reader for binary: %w", err) } - f, err := elf.NewFile(unionReader) + f, err := elfutil.NewFile(unionReader) if f == nil || err != nil { log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to parse binary as ELF") return nil, nil diff --git a/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go b/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go index 115805887..46f42b383 100644 --- a/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go +++ b/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/cobra" "go.yaml.in/yaml/v3" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/internal" "github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/internal/config" ) @@ -203,7 +204,7 @@ const ( ) func getPlatformElf(f *os.File) string { - elfFile, err := elf.NewFile(f) + elfFile, err := elfutil.NewFile(f) if err != nil { return "" } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index f1a905673..90ce268be 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -3,7 +3,6 @@ package golang import ( "bytes" "context" - "debug/elf" "debug/macho" "debug/pe" "errors" @@ -22,6 +21,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" @@ -398,7 +398,7 @@ func getGOARCHFromBin(r io.ReaderAt) (string, error) { var arch string switch { case bytes.HasPrefix(ident, []byte("\x7FELF")): - f, err := elf.NewFile(r) + f, err := elfutil.NewFile(r) if err != nil { return "", fmt.Errorf("unrecognized file format: %w", err) } diff --git a/syft/pkg/cataloger/golang/scan_binary.go b/syft/pkg/cataloger/golang/scan_binary.go index a4841a27c..e88aa91f2 100644 --- a/syft/pkg/cataloger/golang/scan_binary.go +++ b/syft/pkg/cataloger/golang/scan_binary.go @@ -12,6 +12,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" ) @@ -122,6 +123,17 @@ func getNativeFIPSSettings(settings []debug.BuildSetting) []string { return cryptoSettings } +// readBuildInfo bounds the reader before handing it to debug/buildinfo, which opens ELF files with +// debug/elf itself rather than through elfutil. elf.NewFile expands the section-name string table as it +// parses, so an unbounded read here is reachable no matter how little of the file buildinfo goes on to +// look at. +func readBuildInfo(r io.ReaderAt) (*debug.BuildInfo, error) { + if err := elfutil.CheckSectionNameTable(r); err != nil { + return nil, err + } + return buildinfo.Read(r) +} + func getBuildInfo(r io.ReaderAt, location file.Location) (bi *debug.BuildInfo, err error) { defer func() { if r := recover(); r != nil { @@ -133,7 +145,7 @@ func getBuildInfo(r io.ReaderAt, location file.Location) (bi *debug.BuildInfo, e }() // try to read buildinfo from the binary directly - bi, err = buildinfo.Read(r) + bi, err = readBuildInfo(r) if err == nil { return bi, nil } @@ -144,7 +156,7 @@ func getBuildInfo(r io.ReaderAt, location file.Location) (bi *debug.BuildInfo, e log.WithFields("path", location.RealPath).Trace("detected UPX-compressed Go binary, attempting decompression to read the build info") decompressed, decompErr := decompressUPX(r) if decompErr == nil { - bi, err = buildinfo.Read(decompressed) + bi, err = readBuildInfo(decompressed) if err == nil { return bi, nil } diff --git a/syft/pkg/cataloger/golang/scan_binary_bomb_test.go b/syft/pkg/cataloger/golang/scan_binary_bomb_test.go new file mode 100644 index 000000000..a6dc20803 --- /dev/null +++ b/syft/pkg/cataloger/golang/scan_binary_bomb_test.go @@ -0,0 +1,101 @@ +package golang + +import ( + "bytes" + "compress/zlib" + "debug/buildinfo" + "debug/elf" + "encoding/binary" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" +) + +// Test_getBuildInfo_compressedSectionBomb covers the reason readBuildInfo exists: debug/buildinfo opens +// ELF files with debug/elf itself, and elf.NewFile expands the section-name string table as it parses, +// so an oversized compression header there is an unbounded allocation on a path elfutil.NewFile never +// sees. The fixture is a real zlib stream, so it delivers every byte its header promises. +func Test_getBuildInfo_compressedSectionBomb(t *testing.T) { + const declared = 256 << 20 // comfortably over elfutil's bound, small enough to allocate in a test + + bomb := elfWithCompressedNameTable(t, declared) + t.Logf("%d byte fixture declares a %d byte section name table", len(bomb), declared) + + // the unguarded path is the thing being defended against: prove the fixture really is a bomb + unguarded := measureAlloc(t, func() { + _, err := buildinfo.Read(bytes.NewReader(bomb)) + t.Logf("buildinfo.Read err: %v", err) + }) + assert.Greater(t, unguarded, uint64(declared), "fixture did not actually deliver the declared bytes") + + guarded := measureAlloc(t, func() { + _, err := getBuildInfo(bytes.NewReader(bomb), file.NewLocation("bomb")) + require.Error(t, err) + assert.Contains(t, err.Error(), "over the") + }) + assert.Less(t, guarded, uint64(32<<20), "getBuildInfo allocated far more than the input warrants") + t.Logf("unguarded allocated %d bytes, guarded allocated %d bytes", unguarded, guarded) +} + +func measureAlloc(t *testing.T, fn func()) uint64 { + t.Helper() + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + fn() + runtime.ReadMemStats(&after) + return after.TotalAlloc - before.TotalAlloc +} + +// elfWithCompressedNameTable builds a minimal ELF64 whose only real section is a SHF_COMPRESSED +// .shstrtab declaring `declared` decompressed bytes and genuinely delivering them. +func elfWithCompressedNameTable(t *testing.T, declared uint64) []byte { + t.Helper() + + // the decompressed name table only has to start with the section names; the rest is padding that + // exists purely to make the declared size real + payload := make([]byte, declared) + copy(payload, "\x00.shstrtab\x00") + + var compressed bytes.Buffer + zw := zlib.NewWriter(&compressed) + _, err := zw.Write(payload) + require.NoError(t, err) + require.NoError(t, zw.Close()) + + ehsize := uint64(binary.Size(elf.Header64{})) + shentsize := uint64(binary.Size(elf.Section64{})) + chdrsize := uint64(binary.Size(elf.Chdr64{})) + shoff := ehsize + bodyOff := shoff + 2*shentsize + + var ident [16]byte + copy(ident[:], elf.ELFMAG) + ident[elf.EI_CLASS] = byte(elf.ELFCLASS64) + ident[elf.EI_DATA] = byte(elf.ELFDATA2LSB) + ident[elf.EI_VERSION] = byte(elf.EV_CURRENT) + + buf := &bytes.Buffer{} + require.NoError(t, binary.Write(buf, binary.LittleEndian, elf.Header64{ + Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(elf.EM_X86_64), + Version: uint32(elf.EV_CURRENT), Shoff: shoff, Ehsize: uint16(ehsize), + Shentsize: uint16(shentsize), Shnum: 2, Shstrndx: 1, + })) + // the null section + require.NoError(t, binary.Write(buf, binary.LittleEndian, elf.Section64{})) + // the name table: sh_size is the on-disk size, so it has to cover the whole zlib stream + require.NoError(t, binary.Write(buf, binary.LittleEndian, elf.Section64{ + Name: 1, Type: uint32(elf.SHT_STRTAB), Flags: uint64(elf.SHF_COMPRESSED), + Off: bodyOff, Size: chdrsize + uint64(compressed.Len()), Addralign: 1, + })) + require.NoError(t, binary.Write(buf, binary.LittleEndian, elf.Chdr64{ + Type: uint32(elf.COMPRESS_ZLIB), Size: declared, Addralign: 1, + })) + buf.Write(compressed.Bytes()) + + return buf.Bytes() +} diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go index cd6b8bdd3..f7b523463 100644 --- a/syft/pkg/cataloger/golang/symbols.go +++ b/syft/pkg/cataloger/golang/symbols.go @@ -2,7 +2,6 @@ package golang import ( "bytes" - "debug/elf" "debug/gosym" "debug/macho" "encoding/binary" @@ -11,6 +10,8 @@ import ( "runtime/debug" "slices" "strings" + + "github.com/anchore/syft/syft/internal/elfutil" ) // mainPackage is the import path the linker assigns to the binary's main package. @@ -274,7 +275,7 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) { switch { case strings.HasPrefix(string(ident), "\x7FELF"): - f, err := elf.NewFile(r) + f, err := elfutil.NewFile(r) if err != nil { return nil, 0, fmt.Errorf("unable to parse ELF binary: %w", err) } diff --git a/syft/pkg/cataloger/internal/binutils/classifier.go b/syft/pkg/cataloger/internal/binutils/classifier.go index 011feaeeb..b2c0ff5e6 100644 --- a/syft/pkg/cataloger/internal/binutils/classifier.go +++ b/syft/pkg/cataloger/internal/binutils/classifier.go @@ -6,6 +6,7 @@ import ( "debug/macho" "debug/pe" "encoding/json" + "errors" "fmt" "io" "maps" @@ -22,6 +23,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" ) @@ -365,7 +367,17 @@ func sharedLibraries(context MatcherContext) ([]string, error) { } defer internal.CloseAndLogError(contents, context.Location.RealPath) - e, _ := elf.NewFile(contents) + e, err := elfutil.NewFile(contents) + if err != nil { + // this function tries ELF, then Mach-O, then PE, so "not an ELF" is the expected case and stays + // quiet. debug/elf reports a wrong magic as an *elf.FormatError, but a file too short to hold a + // header as a bare EOF, so both count as "not an ELF" here. Anything else means a real ELF was + // dropped, and nothing downstream would report it. + var fmtErr *elf.FormatError + if !errors.As(err, &fmtErr) && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { + log.WithFields("file", context.Location.RealPath, "error", err).Debug("unable to parse ELF binary") + } + } if e != nil { symbols, err := e.ImportedLibraries() if err != nil { diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go index 8725a7d01..b65cd173e 100644 --- a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go @@ -7,6 +7,7 @@ import ( "errors" "io" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" ) @@ -21,7 +22,7 @@ func ExtractDepsJSONFromELFBundle(r unionreader.UnionReader) (string, error) { } func findBundleHeaderOffsetInELF(r unionreader.UnionReader) (int64, error) { - elfFile, err := elf.NewFile(r) + elfFile, err := elfutil.NewFile(r) if err != nil { return 0, nil } diff --git a/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go b/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go index c9611b9da..5ed5801b5 100644 --- a/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go +++ b/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go @@ -19,6 +19,7 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/format/cyclonedxjson" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" ) @@ -141,10 +142,11 @@ func fileError(filename string, err error) (nativeImage, error) { // newElf reads a Native Image from an ELF executable. func newElf(filename string, r io.ReaderAt) (nativeImage, error) { // First attempt to read an ELF file. - bi, err := elf.NewFile(r) + bi, err := elfutil.NewFile(r) if err != nil { var fmtErr *elf.FormatError + // note: a size rejection from elfutil is not an *elf.FormatError, so it takes the branch below if errors.As(err, &fmtErr) { // this is not an elf file log.WithFields("filename", filename, "error", err).Trace("not an ELF binary") diff --git a/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go b/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go index 064c8c3ce..ab7f7860e 100644 --- a/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go +++ b/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go @@ -2,7 +2,6 @@ package kernel import ( "context" - "debug/elf" "errors" "fmt" "io" @@ -15,6 +14,7 @@ import ( "github.com/anchore/syft/internal/tmpdir" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/elfutil" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" @@ -168,7 +168,7 @@ func parseLinuxKernelModuleMetadata(r unionreader.UnionReader) (p *pkg.LinuxKern p = &pkg.LinuxKernelModule{ Parameters: make(map[string]pkg.LinuxKernelModuleParameter), } - f, err := elf.NewFile(r) + f, err := elfutil.NewFile(r) if err != nil { return nil, err } diff --git a/test/rules/rules.go b/test/rules/rules.go index f6f1a0e15..fb68fb827 100644 --- a/test/rules/rules.go +++ b/test/rules/rules.go @@ -90,3 +90,16 @@ func packagesInRelationshipsAsValues(m dsl.Matcher) { Where(isRelationship(m) && hasPointerType(m)). Report("pointer used as a value for From/To field in artifact.Relationship (use values instead)") } + +// nolint:unused +func noDirectELFOpen(m dsl.Matcher) { + // debug/elf sizes a compressed section's buffer from that section's own header, so opening an + // attacker-supplied binary with it directly is an unbounded allocation. elfutil.NewFile is a + // drop-in that bounds the sections syft can actually reach. + m.Match( + `elf.NewFile($_)`, + `elf.Open($_)`, + ). + Where(!m.File().PkgPath.Matches(`/syft/internal/elfutil$`)). + Report("do not open ELF files with debug/elf directly; use elfutil.NewFile, which bounds declared decompressed section sizes") +} From 360dbc04aa20b74a3f3ac19d30ee85bda0c076cc Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Tue, 18 Aug 2026 11:44:00 -0400 Subject: [PATCH 132/168] chore(deps): update CPE dictionary index (#5189) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index a61f11642..8e09644d1 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -40,6 +40,9 @@ "github.com/charmbracelet/soft-serve": [ "cpe:2.3:a:charm:soft_serve:*:*:*:*:*:go:*:*" ], + "github.com/cli/go-gh": [ + "cpe:2.3:a:cli:go-gh:*:*:*:*:*:go:*:*" + ], "github.com/cloudflare/circl": [ "cpe:2.3:a:cloudflare:circl:*:*:*:*:*:go:*:*" ], @@ -305,6 +308,9 @@ "Parameterized-Remote-Trigger": [ "cpe:2.3:a:jenkins:parameterized_remote_trigger:*:*:*:*:*:jenkins:*:*" ], + "PrioritySorter": [ + "cpe:2.3:a:jenkins:priority_sorter:*:*:*:*:*:jenkins:*:*" + ], "TestComplete": [ "cpe:2.3:a:jenkins:testcomplete_support:*:*:*:*:*:jenkins:*:*" ], @@ -898,6 +904,9 @@ "ec2-deployment-dashboard": [ "cpe:2.3:a:jenkins:deployment_dashboard:*:*:*:*:*:jenkins:*:*" ], + "ec2-fleet": [ + "cpe:2.3:a:jenkins:ec2_fleet:*:*:*:*:*:jenkins:*:*" + ], "echarts-api": [ "cpe:2.3:a:jenkins:echarts_api:*:*:*:*:*:jenkins:*:*" ], @@ -2285,6 +2294,9 @@ "zap-pipeline": [ "cpe:2.3:a:jenkins:zap_pipeline:*:*:*:*:*:jenkins:*:*" ], + "zdevops": [ + "cpe:2.3:a:jenkins:zowe_zdevops:*:*:*:*:*:jenkins:*:*" + ], "zephyr-enterprise-test-management": [ "cpe:2.3:a:jenkins:zephyr_enterprise_test_management:*:*:*:*:*:jenkins:*:*" ], @@ -2974,6 +2986,9 @@ "blamer": [ "cpe:2.3:a:blamer_project:blamer:*:*:*:*:*:node.js:*:*" ], + "bluebird": [ + "cpe:2.3:a:bluebirdjs:bluebird:*:*:*:*:*:node.js:*:*" + ], "bmoor": [ "cpe:2.3:a:bmoor_project:bmoor:*:*:*:*:*:node.js:*:*" ], @@ -3132,6 +3147,9 @@ "closurecompiler": [ "cpe:2.3:a:closurecompiler_project:closurecompiler:*:*:*:*:*:node.js:*:*" ], + "cloudinary": [ + "cpe:2.3:a:cloudinary:cloudinary:*:*:*:*:*:node.js:*:*" + ], "cloudpub-redis": [ "cpe:2.3:a:cloudpub-redis_project:cloudpub-redis:*:*:*:*:*:node.js:*:*" ], @@ -4182,6 +4200,9 @@ "jdf-sass": [ "cpe:2.3:a:jdf-sass_project:jdf-sass:*:*:*:*:*:node.js:*:*" ], + "jest": [ + "cpe:2.3:a:openjsf:jest:*:*:*:*:*:node.js:*:*" + ], "jikes": [ "cpe:2.3:a:jikes_project:jikes:*:*:*:*:*:node.js:*:*" ], @@ -4373,6 +4394,9 @@ "langchain": [ "cpe:2.3:a:langchain:langchain.js:*:*:*:*:*:*:*:*" ], + "langsmith": [ + "cpe:2.3:a:langchain:langsmith:*:*:*:*:*:node.js:*:*" + ], "larvitbase-api": [ "cpe:2.3:a:larvit:larvitbase:*:*:*:*:*:node.js:*:*" ], @@ -4588,6 +4612,9 @@ "mcstatic": [ "cpe:2.3:a:mcstatic_project:mcstatic:*:*:*:*:*:node.js:*:*" ], + "md-fileserver": [ + "cpe:2.3:a:commenthol:md-fileserver:*:*:*:*:*:node.js:*:*" + ], "md-to-pdf": [ "cpe:2.3:a:markdown_to_pdf_project:markdown_to_pdf:*:*:*:*:*:node.js:*:*" ], @@ -5411,6 +5438,15 @@ "react-router": [ "cpe:2.3:a:shopify:react-router:*:*:*:*:*:node.js:*:*" ], + "react-server-dom-parcel": [ + "cpe:2.3:a:facebook:react-server-dom-parcel:*:*:*:*:*:node.js:*:*" + ], + "react-server-dom-turbopack": [ + "cpe:2.3:a:facebook:react-server-dom-turbopack:*:*:*:*:*:node.js:*:*" + ], + "react-server-dom-webpack": [ + "cpe:2.3:a:facebook:react-server-dom-webpack:*:*:*:*:*:node.js:*:*" + ], "realms-shim": [ "cpe:2.3:a:agoric:realms-shim:*:*:*:*:*:node.js:*:*" ], @@ -6336,6 +6372,12 @@ "shopware/shopware": [ "cpe:2.3:a:shopware:shopware:*:*:*:*:*:*:*:*" ], + "symfony/twig-bridge": [ + "cpe:2.3:a:symfony:twig-bridge:*:*:*:*:*:*:*:*" + ], + "symfony/twig-bundle": [ + "cpe:2.3:a:symfony:twig-bundle:*:*:*:*:*:*:*:*" + ], "typo3/html-sanitizer": [ "cpe:2.3:a:typo3:html_sanitizer:*:*:*:*:*:*:*:*" ], @@ -6473,6 +6515,12 @@ "awslabs.aws-api-mcp-server": [ "cpe:2.3:a:amazon:aws_api_mcp_server:*:*:*:*:*:python:*:*" ], + "awslabs.aws-transform-mcp-server": [ + "cpe:2.3:a:amazon:aws_transform_mcp_server:*:*:*:*:*:python:*:*" + ], + "awslabs.documentdb-mcp-server": [ + "cpe:2.3:a:amazon:documentdb_mcp_server:*:*:*:*:*:python:*:*" + ], "azure-core": [ "cpe:2.3:a:microsoft:azure_core_shared_client_library:*:*:*:*:*:python:*:*" ], @@ -7087,6 +7135,9 @@ "untangle": [ "cpe:2.3:a:untangle_project:untangle:*:*:*:*:*:python:*:*" ], + "upsonic": [ + "cpe:2.3:a:upsonic:upsonic:*:*:*:*:*:python:*:*" + ], "urllib3": [ "cpe:2.3:a:python:urllib3:*:*:*:*:*:*:*:*" ], @@ -7114,6 +7165,9 @@ "zeep": [ "cpe:2.3:a:python-zeep:zeep:*:*:*:*:*:python:*:*" ], + "zeroconf": [ + "cpe:2.3:a:paulsm:zeroconf:*:*:*:*:*:python:*:*" + ], "zibalPlatform": [ "cpe:2.3:a:zibal_project:zibal:*:*:*:*:*:pypi:*:*" ] @@ -7718,6 +7772,9 @@ "aws-lc-sys": [ "cpe:2.3:a:amazon:aws-lc-sys:*:*:*:*:*:rust:*:*" ], + "aws-smithy-json": [ + "cpe:2.3:a:amazon:aws-smithy-json:*:*:*:*:*:rust:*:*" + ], "axum-core": [ "cpe:2.3:a:axum-core_project:axum-core:*:*:*:*:*:rust:*:*" ], @@ -9172,6 +9229,9 @@ "ap-mega-menu": [ "cpe:2.3:a:accesspressthemes:ap_mega_menu:*:*:*:*:*:wordpress:*:*" ], + "ap-plugin-scripteo": [ + "cpe:2.3:a:scripteo:ads_pro:*:*:*:*:*:wordpress:*:*" + ], "ap-pricing-tables-lite": [ "cpe:2.3:a:accesspressthemes:ap_pricing_tables_lite:*:*:*:*:*:wordpress:*:*" ], @@ -13874,7 +13934,8 @@ "cpe:2.3:a:kibokolabs:hostel:*:*:*:*:*:wordpress:*:*" ], "hostinger": [ - "cpe:2.3:a:hostinger:hostinger:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:hostinger:hostinger:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:hostinger:hostinger_tools:*:*:*:*:*:wordpress:*:*" ], "hot-linked-image-cacher": [ "cpe:2.3:a:hot_linked_image_cacher_project:hot_linked_image_cacher:*:*:*:*:*:wordpress:*:*" @@ -15253,6 +15314,9 @@ "manual-image-crop": [ "cpe:2.3:a:manual_image_crop_project:manual_image_crop:*:*:*:*:*:wordpress:*:*" ], + "map-addons-for-elementor-waze-map": [ + "cpe:2.3:a:nervythemes:local_business_addons_for_elementor:*:*:*:*:*:wordpress:*:*" + ], "mapfig-studio": [ "cpe:2.3:a:acugis:mapfig_studio:*:*:*:*:*:wordpress:*:*" ], @@ -17540,6 +17604,7 @@ "cpe:2.3:a:inisev:redirection:*:*:*:*:*:wordpress:*:*" ], "redirection": [ + "cpe:2.3:a:johngodley:redirection:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:redirection:redirection:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:redirection_project:redirection:*:*:*:*:*:wordpress:*:*" ], @@ -18376,6 +18441,9 @@ "simple-download-monitor": [ "cpe:2.3:a:tipsandtricks-hq:simple_download_monitor:*:*:*:*:*:wordpress:*:*" ], + "simple-downloads-list": [ + "cpe:2.3:a:neofix:simple_downloads_list:*:*:*:*:*:wordpress:*:*" + ], "simple-e-commerce-shopping-cart": [ "cpe:2.3:a:simple-e-commerce-shopping-cart_project:simple-e-commerce-shopping-cart:*:*:*:*:*:wordpress:*:*" ], @@ -21415,6 +21483,7 @@ "cpe:2.3:a:wow-company:wp_coder:*:*:*:*:*:wordpress:*:*" ], "wp-comment-fields": [ + "cpe:2.3:a:najeebmedia:comments_extra_fields_for_post\\,_pages_and_cpt:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:najeebmedia:wordpress_comments_fields:*:*:*:*:*:wordpress:*:*" ], "wp-commentnavi": [ @@ -22222,7 +22291,7 @@ "cpe:2.3:a:bootstrapped:wp_recipe_maker:*:*:*:*:free:wordpress:*:*" ], "wp-registration": [ - "cpe:2.3:a:najeebmedia:simple_user_registration:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:najeebmedia:memberhero:*:*:*:*:*:wordpress:*:*" ], "wp-remote-site-search": [ "cpe:2.3:a:brainstormforce:wp_remote_site_search:*:*:*:*:*:wordpress:*:*" @@ -23253,6 +23322,9 @@ "activello": [ "cpe:2.3:a:colorlib:activello:*:*:*:*:*:wordpress:*:*" ], + "adforest": [ + "cpe:2.3:a:scriptsbundle:adforest:*:*:*:*:*:wordpress:*:*" + ], "affluent": [ "cpe:2.3:a:cpothemes:affluent:*:*:*:*:*:wordpress:*:*" ], From ab508169e6d316b04ed3fd44780db30a03394a28 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:11:10 -0300 Subject: [PATCH 133/168] fix(golang): skip remote license lookup for standard library module paths (#5192) With search-remote-licenses enabled, every module name went to the proxy, including toolchain binaries whose main module is synthesized from the package path, such as cmd/cgo. The proxy answers 404, and the direct fallback then treats the path as a repository host, producing requests like https://cmd/cgo/info/refs?service=git-upload-pack. Reuse isStandardImportPath, already in this package, to skip the remote search for paths whose first element carries no dot. Those are never publishable module paths, so neither a proxy nor a repository can resolve them. Fixes #3149 Signed-off-by: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> --- syft/pkg/cataloger/golang/licenses.go | 5 ++-- syft/pkg/cataloger/golang/licenses_test.go | 30 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/golang/licenses.go b/syft/pkg/cataloger/golang/licenses.go index cb5a67c0d..e3c40af5d 100644 --- a/syft/pkg/cataloger/golang/licenses.go +++ b/syft/pkg/cataloger/golang/licenses.go @@ -111,8 +111,9 @@ func (c *goLicenseResolver) getLicenses(ctx context.Context, resolver file.Resol } } - // download from remote sources - if c.opts.SearchRemoteLicenses { + // download from remote sources; standard library and toolchain paths are not publishable + // module paths, so neither a proxy nor a repository has anything to resolve for them + if c.opts.SearchRemoteLicenses && !isStandardImportPath(moduleName) { pkgLicenses, err = c.getLicensesFromRemote(ctx, moduleName, moduleVersion) if err != nil { log.WithFields("error", err, "module", moduleName, "version", moduleVersion).Debug("unable to read golang licenses remote") diff --git a/syft/pkg/cataloger/golang/licenses_test.go b/syft/pkg/cataloger/golang/licenses_test.go index 0674b0449..2eb9aa463 100644 --- a/syft/pkg/cataloger/golang/licenses_test.go +++ b/syft/pkg/cataloger/golang/licenses_test.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "strings" + "sync/atomic" "testing" "github.com/stretchr/testify/require" @@ -235,6 +236,35 @@ func Test_LicenseSearch(t *testing.T) { } } +func Test_remoteLicenseSearchSkipsStandardLibrary(t *testing.T) { + ctx := pkgtest.Context(t) + + var requests atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + // module paths whose first element carries no dot are never publishable, so there is + // nothing for a proxy or a repository to resolve + for _, moduleName := range []string{"cmd/cgo", "std", "runtime", "internal/abi", "command-line-arguments"} { + t.Run(moduleName, func(t *testing.T) { + requests.Store(0) + + l := newGoLicenseResolver("", CatalogerConfig{ + SearchRemoteLicenses: true, + Proxies: []string{server.URL}, + }) + + lics := l.getLicenses(ctx, fileresolver.Empty{}, moduleName, "(devel)") + + require.Empty(t, lics) + require.Zero(t, requests.Load(), "expected no remote lookup for a standard library module path") + }) + } +} + func Test_processCaps(t *testing.T) { tests := []struct { name string From ed76e9674926aa51129c488b7f4bbc648bca7097 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 19 Aug 2026 13:13:42 -0400 Subject: [PATCH 134/168] fix: panic when scanning squashfs symlinks (#5119) Signed-off-by: Keith Zantow --- syft/source/snapsource/snap_source.go | 22 ++--- syft/source/snapsource/snap_source_test.go | 107 +++++++++++++++++++++ 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/syft/source/snapsource/snap_source.go b/syft/source/snapsource/snap_source.go index 962e57e3c..38ad3a16d 100644 --- a/syft/source/snapsource/snap_source.go +++ b/syft/source/snapsource/snap_source.go @@ -314,19 +314,7 @@ func squashfsVisitor(ft filetree.Writer, fileCatalog *image.FileCatalog, size *i prog.AtomicStage.Set(path) - var f filesystem.File var mimeType string - var err error - - if !d.IsDir() { - f, err = fsys.OpenFile(intFile.ToFSPath(path), os.O_RDONLY) - if err != nil { - log.WithFields("error", err, "path", path).Trace("unable to open squash file path") - } else { - defer f.Close() - mimeType = stereoFile.MIMEType(f) - } - } var ty stereoFile.Type var linkPath string @@ -335,11 +323,21 @@ func squashfsVisitor(ft filetree.Writer, fileCatalog *image.FileCatalog, size *i // in some implementations, the mode does not indicate a directory, so we check the FileInfo type explicitly ty = stereoFile.TypeDirectory default: + f, err := fsys.OpenFile(intFile.ToFSPath(path), os.O_RDONLY) + if err != nil { + log.WithFields("error", err, "path", path).Trace("unable to open squash file path") + } + if f != nil { + defer f.Close() + } + ty = stereoFile.TypeFromMode(d.Mode()) if ty == stereoFile.TypeSymLink && f != nil { if l, ok := f.(linker); ok { linkPath, _ = l.Readlink() } + } else { + mimeType = stereoFile.MIMEType(f) } } diff --git a/syft/source/snapsource/snap_source_test.go b/syft/source/snapsource/snap_source_test.go index 4515f9c47..6a4b23e4e 100644 --- a/syft/source/snapsource/snap_source_test.go +++ b/syft/source/snapsource/snap_source_test.go @@ -1,23 +1,31 @@ package snapsource import ( + "bytes" "crypto" "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" "sync" "testing" + "time" diskFile "github.com/diskfs/go-diskfs/backend/file" + "github.com/diskfs/go-diskfs/filesystem" "github.com/diskfs/go-diskfs/filesystem/squashfs" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/wagoodman/go-progress" "github.com/anchore/go-homedir" + stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/event/monitor" "github.com/anchore/syft/syft/source" ) @@ -294,3 +302,102 @@ func writeSquashfs(t *testing.T, manifest string) string { require.NoError(t, fs.Finalize(squashfs.FinalizeOptions{})) return snapPath } + +func Test_SquashfsSymlinkCrash(t *testing.T) { + tests := []struct { + name string + path string + info fakeFileInfo + file *fakeSquashFile + wantType stereoFile.Type + wantLinkDest string + wantMIME assert.ValueAssertionFunc + }{ + { + // regression: previously the visitor opened every non-dir file and computed its MIME type + // before inspecting the type, which consumed the symlink's reader so Readlink returned + // nothing and the symlink was given a bogus MIME type. The fix only reads the link for + // symlinks and only computes a MIME type for everything else. + name: "symlink resolves link destination and is not given a mime type", + path: "/link", + info: fakeFileInfo{name: "link", mode: os.ModeSymlink | 0o777}, + // the reader returns content so that the (buggy) behavior of computing a MIME type would + // have produced a non-empty MIME type, making this test fail against the old code. + file: &fakeSquashFile{Reader: bytes.NewReader([]byte("/usr/bin/target")), linkTarget: "/usr/bin/target"}, + wantType: stereoFile.TypeSymLink, + wantLinkDest: "/usr/bin/target", + wantMIME: assert.Empty, + }, + { + name: "regular file is given a mime type", + path: "/file.txt", + info: fakeFileInfo{name: "file.txt", mode: 0o644, size: 11}, + file: &fakeSquashFile{Reader: bytes.NewReader([]byte("hello world"))}, + wantType: stereoFile.TypeRegular, + wantMIME: assert.NotEmpty, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree := filetree.New() + catalog := image.NewFileCatalog() + prog := &monitor.TaskProgress{ + AtomicStage: progress.NewAtomicStage(""), + Manual: progress.NewManual(-1), + } + + fsys := fakeSquashFS{openFile: func(string, int) (filesystem.File, error) { + return tt.file, nil + }} + + visit := squashfsVisitor(tree, catalog, nil, prog) + require.NoError(t, visit(fsys, tt.path, tt.info, nil)) + + entries, err := catalog.GetByBasename(tt.info.name) + require.NoError(t, err) + require.Len(t, entries, 1) + + entry := entries[0] + assert.Equal(t, tt.wantType, entry.Metadata.Type) + assert.Equal(t, tt.wantLinkDest, entry.Metadata.LinkDestination) + tt.wantMIME(t, entry.Metadata.MIMEType) + }) + } +} + +// fakeFileInfo is a minimal os.FileInfo for driving squashfsVisitor in tests. +type fakeFileInfo struct { + name string + mode os.FileMode + size int64 +} + +func (f fakeFileInfo) Name() string { return f.name } +func (f fakeFileInfo) Size() int64 { return f.size } +func (f fakeFileInfo) Mode() os.FileMode { return f.mode } +func (f fakeFileInfo) ModTime() time.Time { return time.Time{} } +func (f fakeFileInfo) IsDir() bool { return f.mode.IsDir() } +func (f fakeFileInfo) Sys() any { return nil } + +// fakeSquashFile implements filesystem.File and the linker interface used by squashfsVisitor. +type fakeSquashFile struct { + *bytes.Reader + linkTarget string +} + +func (f *fakeSquashFile) Write([]byte) (int, error) { return 0, fs.ErrInvalid } +func (f *fakeSquashFile) Close() error { return nil } +func (f *fakeSquashFile) Stat() (fs.FileInfo, error) { return nil, fs.ErrInvalid } +func (f *fakeSquashFile) Readlink() (string, error) { return f.linkTarget, nil } + +// fakeSquashFS implements filesystem.FileSystem but only supports OpenFile; all other methods +// are inherited from the embedded nil interface and will panic if unexpectedly called. +type fakeSquashFS struct { + filesystem.FileSystem + openFile func(string, int) (filesystem.File, error) +} + +func (f fakeSquashFS) OpenFile(name string, flag int) (filesystem.File, error) { + return f.openFile(name, flag) +} From 21cca30e5be80b5e7ca21904a6f8362cfd9456a1 Mon Sep 17 00:00:00 2001 From: Oleksandr Vodotiiets Date: Wed, 19 Aug 2026 21:01:20 +0300 Subject: [PATCH 135/168] perf: make orderedIDSet membership checks constant time (#5178) orderedIDSet.add scanned the entire existing slice for every inserted ID, so building a Collection was quadratic in the number of packages sharing an index key. This is most visible in idsByType: package types are few, so that index accumulates nearly every package in the SBOM into a single set, and each Add rescans it. Keep the ordered slice (ordering and dedup semantics are unchanged) and add a hash index for membership checks. The index is only built once a set grows beyond a small threshold, so the many single-element sets held by idsByName and idsByPath do not pay for a map, while the large idsByType sets get constant-time lookups. BenchmarkCollectionAdd (Apple M2 Pro), packages sharing one type: packages before after speedup 1,000 1.37 ms 0.76 ms 1.8x 10,000 100.31 ms 9.80 ms 10.2x 50,000 2,423.79 ms 34.46 ms 70.3x The tradeoff is memory: sets past the threshold allocate an index map, about 14% more bytes at 50,000 packages, with allocation counts unchanged. Signed-off-by: Oleksandr Vodotiiets <61548316+avodotiiets@users.noreply.github.com> --- syft/pkg/collection.go | 48 ++++++++++++++++++++++--- syft/pkg/collection_test.go | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 5 deletions(-) diff --git a/syft/pkg/collection.go b/syft/pkg/collection.go index d6956c084..215e7eedf 100644 --- a/syft/pkg/collection.go +++ b/syft/pkg/collection.go @@ -299,19 +299,56 @@ func (c *Collection) Sorted(types ...Type) (pkgs []Package) { return pkgs } +// orderedIDSetIndexThreshold is the number of elements beyond which an orderedIDSet maintains a hash +// index for membership checks. Below this size a linear scan of the contiguous slice is competitive +// with a map lookup and avoids allocating a map for the many small (often single-element) sets held +// by the name and path indexes. Above it, the linear scan is what makes bulk insertion quadratic. +const orderedIDSetIndexThreshold = 16 + type orderedIDSet struct { slice []artifact.ID + + // index is nil until the set grows beyond orderedIDSetIndexThreshold, after which it is kept in + // sync with slice and used to answer membership checks in constant time. + index map[artifact.ID]struct{} +} + +func (s *orderedIDSet) contains(id artifact.ID) bool { + if s.index != nil { + _, exists := s.index[id] + return exists + } + + for _, existingID := range s.slice { + if existingID == id { + return true + } + } + + return false } func (s *orderedIDSet) add(ids ...artifact.ID) { -loopNewIDs: for _, newID := range ids { - for _, existingID := range s.slice { - if existingID == newID { - continue loopNewIDs - } + if s.contains(newID) { + continue } + s.slice = append(s.slice, newID) + + switch { + case s.index != nil: + s.index[newID] = struct{}{} + case len(s.slice) > orderedIDSetIndexThreshold: + s.buildIndex() + } + } +} + +func (s *orderedIDSet) buildIndex() { + s.index = make(map[artifact.ID]struct{}, len(s.slice)) + for _, id := range s.slice { + s.index[id] = struct{}{} } } @@ -319,6 +356,7 @@ func (s *orderedIDSet) delete(id artifact.ID) { for i, existingID := range s.slice { if existingID == id { s.slice = append(s.slice[:i], s.slice[i+1:]...) + delete(s.index, id) // no-op when the index has not been built return } } diff --git a/syft/pkg/collection_test.go b/syft/pkg/collection_test.go index 14f698c15..0d8c510ec 100644 --- a/syft/pkg/collection_test.go +++ b/syft/pkg/collection_test.go @@ -2,6 +2,7 @@ package pkg import ( "context" + "fmt" "testing" "github.com/scylladb/go-set/strset" @@ -482,3 +483,73 @@ func Test_idOrderedSet_add(t *testing.T) { }) } } + +func Test_idOrderedSet_addBeyondIndexThreshold(t *testing.T) { + // exercise the indexed path: sets larger than orderedIDSetIndexThreshold must still deduplicate + // and retain insertion ordering + const count = orderedIDSetIndexThreshold * 4 + + var expected []artifact.ID + var input []artifact.ID + for i := 0; i < count; i++ { + id := artifact.ID(fmt.Sprintf("id-%d", i)) + expected = append(expected, id) + input = append(input, id) + } + // add every element a second time; none of these should be appended + input = append(input, expected...) + + var s orderedIDSet + s.add(input...) + + assert.Equal(t, expected, s.slice) + require.NotNil(t, s.index, "index should be built once the set grows beyond the threshold") + assert.Len(t, s.index, len(expected), "index and slice should agree") +} + +func Test_idOrderedSet_deleteKeepsIndexInSync(t *testing.T) { + const count = orderedIDSetIndexThreshold * 2 + + var s orderedIDSet + for i := 0; i < count; i++ { + s.add(artifact.ID(fmt.Sprintf("id-%d", i))) + } + require.NotNil(t, s.index) + + removed := artifact.ID("id-0") + s.delete(removed) + + assert.Len(t, s.slice, count-1) + assert.NotContains(t, s.slice, removed) + assert.False(t, s.contains(removed), "deleted element must not remain in the index") + + // re-adding must work (it would be skipped if the index still held a stale entry) + s.add(removed) + assert.Equal(t, removed, s.slice[len(s.slice)-1]) +} + +func BenchmarkCollectionAdd(b *testing.B) { + // packages sharing a type all land in the same idsByType set, which is the case that previously + // made insertion quadratic + for _, count := range []int{1_000, 10_000, 50_000} { + pkgs := make([]Package, count) + for i := range pkgs { + pkgs[i] = Package{ + Name: fmt.Sprintf("package-%d", i), + Version: "1.0.0", + Type: NpmPkg, + } + pkgs[i].SetID() + } + + b.Run(fmt.Sprintf("packages=%d", count), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + c := NewCollection() + for _, p := range pkgs { + c.Add(p) + } + } + }) + } +} From 8bb3b5eed118835067e011a63513021d567635fe Mon Sep 17 00:00:00 2001 From: Hamody We Date: Wed, 19 Aug 2026 21:09:51 +0300 Subject: [PATCH 136/168] fix(javascript): read every document of a pnpm-lock.yaml stream (#5188) * fix(javascript): read every document of a pnpm-lock.yaml stream pnpm-lock.yaml can be a multi-document YAML stream. pnpm keeps config dependencies and the pinned package-manager version in a leading document and the project's dependency graph in the next one. That layout has shipped since pnpm 11 for projects using config dependencies, and pnpm 12 records the package-manager pin by default, so two documents is now the common case. The cataloger called yaml.Unmarshal, which reads the first document only. On such a lockfile the SBOM contains pnpm's own release binaries and none of the project's dependencies. Both documents declare the same lockfileVersion and the result is well-formed and non-empty, so nothing signals that it is wrong. Split the stream and parse each document, merging the results. A single-document lockfile yields exactly one document, so its behaviour is unchanged. A document that omits lockfileVersion falls back to the version from the first one rather than being dropped. Fixes #5168 Signed-off-by: hamodywe * refactor(javascript): stream pnpm-lock.yaml documents instead of buffering The multi-document fix read the whole lockfile, decoded it to a `yaml.Node` tree, re-marshaled every document back to `[]byte` and then re-parsed each one twice. `yaml.Node` already has a `Decode` method, so the decoder can walk the stream straight off the reader and hand each document's node to the parser. On a 4.8MB single-document lockfile that takes total allocations from 1069MB to 250MB, which is also below where things sat before multi-document support. It drops the `io.ReadAll` and its `noUnboundedReads` suppression, and puts pnpm in line with every other YAML cataloger in the tree, all of which already stream. `pnpmLockfileParser.Parse` now takes a `*yaml.Node` rather than `[]byte`. It is package-private with two implementations and one production call site. Documents accumulate into a single map keyed by `name@version`, so the stream follows one collision rule rather than two: the last entry to appear wins, which is what already happens within a document. That replaces the separate first-wins-with-fill-in merge pass. A conflicting integrity between documents is now traced rather than resolved silently. Three behaviour fixes come with it: - an empty or comment-only leading document no longer takes down the whole file. It decodes to a null node, not a zero-kind one, so the old guard never fired and the stream failed with `invalid lockfile version ""`. - a malformed document no longer discards the documents that already parsed. It is reported through `unknown` instead, matching how the rest of the cataloger reports partial results. - parse errors carry the document index, and their line numbers again point at the user's file rather than into a re-marshaled buffer. Signed-off-by: Alex Goodman * chore(javascript): trim the duplicated pnpm multi-document comment The six-line explanation of why a pnpm lockfile can hold two documents sat verbatim in both the parser and its test. Keep it next to the code, where someone debugging will be, and leave the test pointing at it. Signed-off-by: Alex Goodman --------- Signed-off-by: hamodywe Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- .../cataloger/javascript/parse_pnpm_lock.go | 142 ++++++++--- .../javascript/parse_pnpm_lock_test.go | 222 +++++++++++++++++- .../testdata/pnpm-multi-doc/pnpm-lock.yaml | 57 +++++ 3 files changed, 385 insertions(+), 36 deletions(-) create mode 100644 syft/pkg/cataloger/javascript/testdata/pnpm-multi-doc/pnpm-lock.yaml diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go index deb6a7e3e..4d83ecfa6 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go @@ -3,6 +3,7 @@ package javascript import ( "cmp" "context" + "errors" "fmt" "io" "iter" @@ -33,8 +34,10 @@ type pnpmPackage struct { } // pnpmLockfileParser defines the interface for parsing different versions of pnpm lockfiles. +// Implementations decode into themselves, so a parser is single-use: construct a new one +// per document rather than reusing one across a multi-document stream. type pnpmLockfileParser interface { - Parse(version float64, data []byte) ([]pnpmPackage, error) + Parse(version float64, doc *yaml.Node) ([]pnpmPackage, error) } type pnpmV6PackageEntry struct { @@ -81,8 +84,8 @@ func newGenericPnpmLockAdapter(cfg CatalogerConfig) genericPnpmLockAdapter { } // Parse implements the pnpmLockfileParser interface for v6-v8 lockfiles. -func (p *pnpmV6LockYaml) Parse(version float64, data []byte) ([]pnpmPackage, error) { - if err := yaml.Unmarshal(data, p); err != nil { +func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage, error) { + if err := doc.Decode(p); err != nil { return nil, fmt.Errorf("failed to unmarshal pnpm v6 lockfile: %w", err) } @@ -131,8 +134,8 @@ func (p *pnpmV6LockYaml) Parse(version float64, data []byte) ([]pnpmPackage, err } // Parse implements the PnpmLockfileParser interface for v9+ lockfiles. -func (p *pnpmV9LockYaml) Parse(_ float64, data []byte) ([]pnpmPackage, error) { - if err := yaml.Unmarshal(data, p); err != nil { +func (p *pnpmV9LockYaml) Parse(_ float64, doc *yaml.Node) ([]pnpmPackage, error) { + if err := doc.Decode(p); err != nil { return nil, fmt.Errorf("failed to unmarshal pnpm v9 lockfile: %w", err) } @@ -183,38 +186,117 @@ func newPnpmLockfileParser(version float64) pnpmLockfileParser { // parsePnpmLock is the main parser function for pnpm-lock.yaml files. func (a genericPnpmLockAdapter) parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - data, err := io.ReadAll(reader) //nolint:gocritic // multi-pass parse requires []byte - if err != nil { - return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err) - } + // pnpm-lock.yaml can be a multi-document YAML stream: pnpm keeps config dependencies + // and the pinned package-manager version in a leading document and the project's + // dependency graph in the next one. Reading only the first document yields a + // well-formed SBOM that contains pnpm's own release binaries and none of the + // project's dependencies, with nothing to signal that it is wrong. + // See https://github.com/anchore/syft/issues/5168. + // + // Every document is cataloged, not just the project's. The packages pnpm records for + // itself (pnpm and its @pnpm/exe.* release binaries) are really installed on disk, so + // they are reported as components like any other dependency. The alternative, keeping + // only the last document, would drop them from the SBOM entirely. + pnpmPkgs, errs := parsePnpmLockStream(reader) - var lockfile struct { - Version string `yaml:"lockfileVersion"` - } - if err := yaml.Unmarshal(data, &lockfile); err != nil { - return nil, nil, fmt.Errorf("failed to parse pnpm-lock.yaml version: %w", err) - } - - version, err := strconv.ParseFloat(lockfile.Version, 64) - if err != nil { - return nil, nil, fmt.Errorf("invalid lockfile version %q: %w", lockfile.Version, err) - } - - parser := newPnpmLockfileParser(version) - pnpmPkgs, err := parser.Parse(version, data) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse pnpm-lock.yaml file: %w", err) - } - - packages := make([]pkg.Package, 0, len(pnpmPkgs)) - for _, p := range pnpmPkgs { + // left nil when nothing parses, so a failed lockfile reports no packages rather than an empty set + var packages []pkg.Package + for _, p := range toSortedSlice(pnpmPkgs) { if p.Dev && !a.cfg.IncludeDevDependencies { continue } packages = append(packages, newPnpmPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Integrity, p.Dependencies)) } - return packages, dependency.Resolve(pnpmLockDependencySpecifier, packages), unknown.IfEmptyf(packages, "unable to determine packages") + errs = unknown.Join(errs, unknown.IfEmptyf(packages, "unable to determine packages")) + + return packages, dependency.Resolve(pnpmLockDependencySpecifier, packages), errs +} + +// parsePnpmLockStream reads every document of the lockfile stream, keyed by name@version. +// A document that fails to parse is reported as unknown rather than discarding the +// documents that did parse. +func parsePnpmLockStream(reader file.LocationReadCloser) (map[string]pnpmPackage, error) { + dec := yaml.NewDecoder(reader) + + pnpmPkgs := make(map[string]pnpmPackage) + var firstVersion float64 + var errs error + + for i := 0; ; i++ { + var doc yaml.Node + if err := dec.Decode(&doc); err != nil { + if errors.Is(err, io.EOF) { + break + } + // a malformed document leaves the decoder unusable, so stop reading and keep + // what earlier documents contributed rather than dropping the whole lockfile + errs = unknown.Appendf(errs, reader, "failed to parse pnpm-lock.yaml document %d: %v", i, err) + break + } + + // an empty or comment-only document decodes to a null node; skipping it keeps a + // leading separator from failing the stream on a missing lockfileVersion + if len(doc.Content) == 0 || doc.Content[0].Tag == "!!null" { + continue + } + + version, err := pnpmDocumentVersion(&doc, firstVersion) + if err != nil { + // no document has yielded a usable version yet, so there is nothing to parse + return nil, unknown.Join(errs, err) + } + if firstVersion == 0 { + firstVersion = version + } + + pkgs, err := newPnpmLockfileParser(version).Parse(version, &doc) + if err != nil { + errs = unknown.Appendf(errs, reader, "failed to parse pnpm-lock.yaml document %d: %v", i, err) + continue + } + mergePnpmPackages(pnpmPkgs, pkgs, i) + } + + return pnpmPkgs, errs +} + +// pnpmDocumentVersion reads lockfileVersion from a single document of the stream. +// Documents after the first are expected to repeat it, but one that omits it inherits the +// version already established rather than being dropped. +func pnpmDocumentVersion(doc *yaml.Node, established float64) (float64, error) { + var lockfile struct { + Version string `yaml:"lockfileVersion"` + } + if err := doc.Decode(&lockfile); err != nil { + if established != 0 { + return established, nil + } + return 0, fmt.Errorf("failed to parse pnpm-lock.yaml version: %w", err) + } + + version, err := strconv.ParseFloat(lockfile.Version, 64) + switch { + case err == nil: + return version, nil + case established != 0: + return established, nil + default: + return 0, fmt.Errorf("invalid lockfile version %q: %w", lockfile.Version, err) + } +} + +// mergePnpmPackages folds one document's packages into the running set. The whole stream +// follows a single collision rule, the same one used within a document: the last entry to +// appear wins. +func mergePnpmPackages(into map[string]pnpmPackage, pkgs []pnpmPackage, doc int) { + for _, p := range pkgs { + key := p.Name + "@" + p.Version + if existing, ok := into[key]; ok && existing.Integrity != "" && p.Integrity != "" && existing.Integrity != p.Integrity { + log.WithFields("package", key, "document", doc).Trace("conflicting integrity across pnpm-lock.yaml documents") + } + into[key] = p + } } // parseVersionField extracts the version string from a dependency entry. diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go index b3d2a433c..aca4a7c52 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go @@ -6,10 +6,13 @@ import ( "net/http" "net/http/httptest" "os" + "sort" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -527,6 +530,15 @@ func Test_corruptPnpmLock(t *testing.T) { TestParser(t, adapter.parsePnpmLock) } +// yamlDocument decodes a single YAML document for the parsers, which take the node the +// lockfile stream decoder hands them rather than raw bytes. +func yamlDocument(t *testing.T, data string) *yaml.Node { + t.Helper() + var doc yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(data), &doc)) + return &doc +} + func TestParsePnpmLock_DeterministicWithCollidingPeerDeps(t *testing.T) { // this test verifies that when multiple lockfile keys collapse to the same // package key after peer-dep stripping (e.g., pkg@1.0.0(peer-a@1) and @@ -535,7 +547,7 @@ func TestParsePnpmLock_DeterministicWithCollidingPeerDeps(t *testing.T) { // the last key lexicographically wins. // v9 lockfile with two entries that collapse to the same key - lockfileV9 := []byte(` + lockfileV9 := ` lockfileVersion: '9.0' packages: some-pkg@1.0.0(peer-b@2.0.0): @@ -545,12 +557,12 @@ packages: snapshots: some-pkg@1.0.0(peer-b@2.0.0): {} some-pkg@1.0.0(peer-a@1.0.0): {} -`) +` // run multiple times to catch nondeterminism for range 10 { parser := &pnpmV9LockYaml{} - pkgs, err := parser.Parse(9.0, lockfileV9) + pkgs, err := parser.Parse(9.0, yamlDocument(t, lockfileV9)) require.NoError(t, err) require.Len(t, pkgs, 1, "expected exactly one package after key collision") @@ -561,18 +573,18 @@ snapshots: } // v6 lockfile with two entries that collapse to the same key - lockfileV6 := []byte(` + lockfileV6 := ` lockfileVersion: '6.0' packages: /some-pkg@1.0.0(peer-b@2.0.0): resolution: {integrity: sha512-BBB} /some-pkg@1.0.0(peer-a@1.0.0): resolution: {integrity: sha512-AAA} -`) +` for range 10 { parser := &pnpmV6LockYaml{} - pkgs, err := parser.Parse(6.0, lockfileV6) + pkgs, err := parser.Parse(6.0, yamlDocument(t, lockfileV6)) require.NoError(t, err) require.Len(t, pkgs, 1, "expected exactly one package after key collision") @@ -618,3 +630,201 @@ func setupNpmRegistry() (mux *http.ServeMux, serverURL string, teardown func()) return mux, server.URL, server.Close } + +// covers a real two-document lockfile end to end; parsePnpmLock explains why every +// document is cataloged +func TestParsePnpmLock_MultiDocument(t *testing.T) { + var expectedRelationships []artifact.Relationship + fixture := "testdata/pnpm-multi-doc/pnpm-lock.yaml" + + locationSet := file.NewLocationSet(file.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ + { + Name: "@pnpm/exe.linux-x64", + Version: "12.0.0-rc.3", + PURL: "pkg:npm/%40pnpm/exe.linux-x64@12.0.0-rc.3", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.PnpmLockEntry{ + Resolution: pkg.PnpmLockResolution{Integrity: "sha512-/6xWaYfp6MEaJF+7AZIfCMp/fZX2jZlTLh2KVBEH0QOWk1ktaBlKNIJEgT4m6mieOLhNYMzCfLJImMbJDK1IwA=="}, + Dependencies: map[string]string{}, + }, + }, + { + // The project's actual dependency, which lives in the second document. + Name: "minimist", + Version: "1.2.0", + PURL: "pkg:npm/minimist@1.2.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.PnpmLockEntry{ + Resolution: pkg.PnpmLockResolution{Integrity: "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw=="}, + Dependencies: map[string]string{}, + }, + }, + { + Name: "pnpm", + Version: "12.0.0-rc.3", + PURL: "pkg:npm/pnpm@12.0.0-rc.3", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.PnpmLockEntry{ + Resolution: pkg.PnpmLockResolution{Integrity: "sha512-JZ9fDGH+WLdRdTEikN3UxeZe6bpDY7dYwV0RX0+OrJ927vc72XbId4IJXeT++ebNFA9cF3IQ1swiXHEd/Maq0Q=="}, + Dependencies: map[string]string{}, + }, + }, + } + + adapter := newGenericPnpmLockAdapter(CatalogerConfig{IncludeDevDependencies: true}) + pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) +} + +// pnpmDoc renders a minimal v9 lockfile document holding a single package. +func pnpmDoc(version, name, ver, integrity string) string { + doc := "" + if version != "" { + doc += "lockfileVersion: '" + version + "'\n" + } + return doc + "packages:\n " + name + "@" + ver + ":\n resolution: {integrity: " + integrity + "}\nsnapshots:\n " + name + "@" + ver + ": {}\n" +} + +func TestParsePnpmLock_MultiDocumentStream(t *testing.T) { + project := pnpmDoc("9.0", "minimist", "1.2.0", "sha512-AAA") + + tests := []struct { + name string + lockfile string + wantPkgs []string + wantErr bool + }{ + { + // a stream may open with a separator, which decodes to a null document; it must + // not take down the documents that follow it + name: "leading comment-only document is skipped", + lockfile: "---\n# generated by pnpm\n---\n" + project, + wantPkgs: []string{"minimist@1.2.0"}, + }, + { + name: "trailing separator is skipped", + lockfile: project + "---\n", + wantPkgs: []string{"minimist@1.2.0"}, + }, + { + // documents after the first inherit the established lockfileVersion rather than + // being dropped for omitting one + name: "later document inherits lockfileVersion", + lockfile: project + "---\n" + pnpmDoc("", "left-pad", "1.0.0", "sha512-BBB"), + wantPkgs: []string{"left-pad@1.0.0", "minimist@1.2.0"}, + }, + { + // a broken document is reported, but the documents that already parsed are kept + name: "corrupt later document keeps earlier packages", + lockfile: project + "---\nlockfileVersion: '9.0'\npackages:\n bad@: {oops\n", + wantPkgs: []string{"minimist@1.2.0"}, + wantErr: true, + }, + { + name: "single document is unchanged", + lockfile: project, + wantPkgs: []string{"minimist@1.2.0"}, + }, + { + name: "empty file yields no packages", + lockfile: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adapter := newGenericPnpmLockAdapter(CatalogerConfig{IncludeDevDependencies: true}) + pkgs, _, err := adapter.parsePnpmLock(context.Background(), nil, nil, file.LocationReadCloser{ + Location: file.NewLocation("pnpm-lock.yaml"), + ReadCloser: io.NopCloser(strings.NewReader(tt.lockfile)), + }) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + var got []string + for _, p := range pkgs { + got = append(got, p.Name+"@"+p.Version) + } + sort.Strings(got) + assert.Equal(t, tt.wantPkgs, got) + }) + } +} + +func Test_mergePnpmPackages(t *testing.T) { + // the whole stream shares one collision rule, the same one used within a document: + // the last entry to appear wins + first := pnpmPackage{Name: "a", Version: "1.0.0", Integrity: "sha512-AAA", Dev: true, Dependencies: map[string]string{"b": "2.0.0"}} + second := pnpmPackage{Name: "a", Version: "1.0.0", Integrity: "sha512-BBB"} + + tests := []struct { + name string + docs [][]pnpmPackage + want []pnpmPackage + }{ + { + name: "disjoint documents are unioned", + docs: [][]pnpmPackage{{first}, {{Name: "c", Version: "3.0.0"}}}, + want: []pnpmPackage{first, {Name: "c", Version: "3.0.0"}}, + }, + { + name: "later document wins on a colliding name@version", + docs: [][]pnpmPackage{{first}, {second}}, + want: []pnpmPackage{second}, + }, + { + name: "same package in one document only", + docs: [][]pnpmPackage{{first}}, + want: []pnpmPackage{first}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := make(map[string]pnpmPackage) + for i, doc := range tt.docs { + mergePnpmPackages(got, doc, i) + } + assert.Equal(t, tt.want, toSortedSlice(got)) + }) + } +} + +func Test_pnpmDocumentVersion(t *testing.T) { + tests := []struct { + name string + doc string + established float64 + want float64 + wantErr bool + }{ + {name: "reads its own version", doc: "lockfileVersion: '9.0'\n", want: 9.0}, + {name: "inherits when omitted", doc: "packages: {}\n", established: 9.0, want: 9.0}, + {name: "own version beats the established one", doc: "lockfileVersion: '6.0'\n", established: 9.0, want: 6.0}, + {name: "first document must carry a version", doc: "packages: {}\n", wantErr: true}, + {name: "first document must carry a usable version", doc: "lockfileVersion: 'nope'\n", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := pnpmDocumentVersion(yamlDocument(t, tt.doc), tt.established) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/syft/pkg/cataloger/javascript/testdata/pnpm-multi-doc/pnpm-lock.yaml b/syft/pkg/cataloger/javascript/testdata/pnpm-multi-doc/pnpm-lock.yaml new file mode 100644 index 000000000..b25c7fd63 --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/pnpm-multi-doc/pnpm-lock.yaml @@ -0,0 +1,57 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.0.0-rc.3 + version: 12.0.0-rc.3 + +packages: + + '@pnpm/exe.linux-x64@12.0.0-rc.3': + resolution: {integrity: sha512-/6xWaYfp6MEaJF+7AZIfCMp/fZX2jZlTLh2KVBEH0QOWk1ktaBlKNIJEgT4m6mieOLhNYMzCfLJImMbJDK1IwA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + pnpm@12.0.0-rc.3: + resolution: {integrity: sha512-JZ9fDGH+WLdRdTEikN3UxeZe6bpDY7dYwV0RX0+OrJ927vc72XbId4IJXeT++ebNFA9cF3IQ1swiXHEd/Maq0Q==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.linux-x64@12.0.0-rc.3': + optional: true + + pnpm@12.0.0-rc.3: + optionalDependencies: + '@pnpm/exe.linux-x64': 12.0.0-rc.3 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + minimist: + specifier: 1.2.0 + version: 1.2.0 + +packages: + + minimist@1.2.0: + resolution: {integrity: sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==} + +snapshots: + + minimist@1.2.0: {} From 5c6cf08a8c824f63e8a5e507228b1db80ecbcfce Mon Sep 17 00:00:00 2001 From: CAOShurong <3502119616@qq.com> Date: Thu, 20 Aug 2026 02:15:15 +0800 Subject: [PATCH 137/168] fix(binary): detect multi-arch ingress-nginx (#5179) * fix(binary): detect multi-arch ingress-nginx Add v1.9.6 ARM64, ARMv7, and s390x fixtures from the published controller image and match their version marker against the nearby Go build version. Assisted-by: OpenAI Codex Signed-off-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> * fix(binary): match ingress-nginx release marker on all arches The release is injected with `-ldflags -X`, so it lands in its own aligned, NUL-padded data symbol. Only that padding is portable -- the surrounding bytes are an arch-specific float constant pool, which is why the existing matchers all anchor on incidental junk like `$a` and `S=v` followed by two NULs finds exactly one hit in every v1.9.6 binary (amd64, arm, arm64, s390x) and in all nine amd64 releases already under test. Two NULs matter -- with one, s390x matches a vendored `v1.19.0` earlier in the file and reports the wrong version. This also drops the ~8KB wildcard window the marker previously needed, so the new fixtures are ordinary 369 byte snippets instead of 8.4KB ones. Two things in the fixture tooling had to move for that to work: - `config.yaml` pinned the multi-arch *index* digest for all four platforms, but extraction runs `docker create ` with no `--platform`, so every entry resolved to whichever image was pulled last. `make download` failed outright on `linux/arm/v7`, since the registry labels that platform `armv7` and docker normalizes it to `arm`. Now pinned per-platform, matching the redis entry. - `write-snippet` only recognized EM_X86_64 and EM_AARCH64, so anything else landed in a `linux-unknown-` directory and had to be placed by hand. Verified against the real binaries with `-must-use-original-binaries`. Signed-off-by: Alex Goodman * fix(binary): put non-amd64 snippets where the tests look for them `write-snippet` used to fall back to `unknown-` for any ELF machine it didn't recognize, so three snippets were sitting in directories nothing reads: helm/3.12.0/linux-unknown-454d5f53333930 (hex of "EM_S390") helm/4.1.4/linux-unknown-454d5f53333930 redis-server/7.2.5/linux-unknown-454d5f333836 (hex of "EM_386") The test table asks for `linux-s390x` and `linux-386`, so those three cases were quietly falling through to downloading the full binaries instead. Renamed to match, and taught the tool about EM_386 and EM_RISCV so it stops producing dead directories (there is an existing `linux-riscv64` snippet it also couldn't have written). Signed-off-by: Alex Goodman --------- Signed-off-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> Co-authored-by: Alex Goodman --- .../binary/classifier_cataloger_test.go | 33 ++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 5 +++ .../internal/cli/commands/write_snippet.go | 8 +++++ .../helm | Bin .../helm | Bin .../1.9.6/linux-arm/nginx-ingress-controller | Bin 0 -> 369 bytes .../linux-arm64/nginx-ingress-controller | Bin 0 -> 369 bytes .../linux-s390x/nginx-ingress-controller | Bin 0 -> 369 bytes .../redis-server | Bin .../pkg/cataloger/binary/testdata/config.yaml | 10 +++++- 10 files changed, 55 insertions(+), 1 deletion(-) rename syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/3.12.0/{linux-unknown-454d5f53333930 => linux-s390x}/helm (100%) rename syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/4.1.4/{linux-unknown-454d5f53333930 => linux-s390x}/helm (100%) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/nginx-ingress-controller/1.9.6/linux-arm/nginx-ingress-controller create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/nginx-ingress-controller/1.9.6/linux-arm64/nginx-ingress-controller create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/nginx-ingress-controller/1.9.6/linux-s390x/nginx-ingress-controller rename syft/pkg/cataloger/binary/testdata/classifiers/snippets/redis-server/7.2.5/{linux-unknown-454d5f333836 => linux-386}/redis-server (100%) diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index 4cbd2f6f3..d8d3567ff 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -2613,6 +2613,39 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("ingress-nginx-binary"), }, }, + { + logicalFixture: "nginx-ingress-controller/1.9.6/linux-arm64", + expected: pkg.Package{ + Name: "nginx-ingress-controller", + Version: "1.9.6", + Type: "binary", + PURL: "pkg:generic/nginx-ingress-controller@1.9.6", + Locations: locations("nginx-ingress-controller"), + Metadata: metadata("ingress-nginx-binary"), + }, + }, + { + logicalFixture: "nginx-ingress-controller/1.9.6/linux-arm", + expected: pkg.Package{ + Name: "nginx-ingress-controller", + Version: "1.9.6", + Type: "binary", + PURL: "pkg:generic/nginx-ingress-controller@1.9.6", + Locations: locations("nginx-ingress-controller"), + Metadata: metadata("ingress-nginx-binary"), + }, + }, + { + logicalFixture: "nginx-ingress-controller/1.9.6/linux-s390x", + expected: pkg.Package{ + Name: "nginx-ingress-controller", + Version: "1.9.6", + Type: "binary", + PURL: "pkg:generic/nginx-ingress-controller@1.9.6", + Locations: locations("nginx-ingress-controller"), + Metadata: metadata("ingress-nginx-binary"), + }, + }, { logicalFixture: "nginx-ingress-controller/1.7.1/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index afd4c7536..78f6e18ed 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -1153,6 +1153,11 @@ func DefaultClassifiers() []binutils.Classifier { Class: "ingress-nginx-binary", FileGlob: "**/nginx-ingress-controller", EvidenceMatcher: binutils.MatchAny( + // the release is injected with -ldflags -X, which lands it in its own NUL-padded data symbol. + // the surrounding bytes are an arch-specific float constant pool, so only the padding is portable. + // e.g. v1.9.6[NUL][NUL] on each of linux/amd64, linux/arm, linux/arm64, and linux/s390x + // note: one trailing NUL is not enough -- on s390x that matches a vendored "v1.19.0" earlier in the file + m.FileContentsVersionMatcher(`v(?P[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta)\.[0-9]+)?)\x00\x00`), // [NUL][NUL]v1.15.1[NUL][NUL]@e[ETX][NUL][NUL][NUL][NUL]go1.26.1[NUL][NUL][NUL] // �v1.15.1[NUL][NUL]�z[ETX][NUL][NUL][NUL][NUL]go1.24.4[NUL][NUL][NUL] m.FileContentsVersionMatcher(`v(?P[0-9]+\.[0-9]+\.[0-9]+)\x00+.{0,50}go[0-9]+\.[0-9]+(\-(alpha|beta)\.[0-9])?\.[0-9]+\x00+`), diff --git a/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go b/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go index 46f42b383..5d5f31725 100644 --- a/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go +++ b/syft/pkg/cataloger/binary/internal/manager/internal/cli/commands/write_snippet.go @@ -215,6 +215,14 @@ func getPlatformElf(f *os.File) string { arch = amd64 case elf.EM_AARCH64: arch = arm64 + case elf.EM_ARM: + arch = "arm" + case elf.EM_S390: + arch = "s390x" + case elf.EM_386: + arch = "386" + case elf.EM_RISCV: + arch = "riscv64" // TODO... default: arch = fmt.Sprintf("unknown-%x", elfFile.Machine) diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/3.12.0/linux-unknown-454d5f53333930/helm b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/3.12.0/linux-s390x/helm similarity index 100% rename from syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/3.12.0/linux-unknown-454d5f53333930/helm rename to syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/3.12.0/linux-s390x/helm diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/4.1.4/linux-unknown-454d5f53333930/helm b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/4.1.4/linux-s390x/helm similarity index 100% rename from syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/4.1.4/linux-unknown-454d5f53333930/helm rename to syft/pkg/cataloger/binary/testdata/classifiers/snippets/helm/4.1.4/linux-s390x/helm diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/nginx-ingress-controller/1.9.6/linux-arm/nginx-ingress-controller b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/nginx-ingress-controller/1.9.6/linux-arm/nginx-ingress-controller new file mode 100644 index 0000000000000000000000000000000000000000..748c0bd013e73b24ab15e43f1456b97e1feba2d2 GIT binary patch literal 369 zcmYk1Jx;_h5QQCBK!k>x%F+r^?aCg_qOIf7GUFd$N2 zH|}p47?3&Vo{-FlXcaSzaLJgGlw}$U4>|WBnJ`683d%F788u824>9SY~gp!#4uji+qdc8b7+9n4P?7t7m eEb7N}Ebepm2H8%4p+Mv~d&Zeh(Hw~N1p7QTd5wpxg--~(9L z=o|O~g1vBN_52v-o0)I;n2Gk#7dTF;crvWwWbWH`2uL5 zmJJ32>`pt6muKRx!6ALs9OEQq2%*c1m6TE}R=fRmgl@Nv=*Wy@k`fjPJ*S=DdgmE^ r-oL!P_Ol*0*FB!b__ssoV99^2eXIu{qO96vkPvJ!VmQW5G!*xwnD~h1@9t(J9Y;z+fP7=eWigvO=QJ z)<%j|4f`+^Rp>X3=^aA5P)eaCh7t;|yyvP=PDV6_Tdp#%OOG)c?!d}uOF&8Kcsyps zeF8R|$x_R(|Ej%Zy+qfKg!Dby^yBy{9h^N8awDdGY6)3?lWkoLcr@VaHyIH!``QeA e{j>9(41D(f@cHt5emeVo``9XnJAI$`@BagD&t&BQ literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/redis-server/7.2.5/linux-unknown-454d5f333836/redis-server b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/redis-server/7.2.5/linux-386/redis-server similarity index 100% rename from syft/pkg/cataloger/binary/testdata/classifiers/snippets/redis-server/7.2.5/linux-unknown-454d5f333836/redis-server rename to syft/pkg/cataloger/binary/testdata/classifiers/snippets/redis-server/7.2.5/linux-386/redis-server diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml index 9e6197353..37ed85815 100644 --- a/syft/pkg/cataloger/binary/testdata/config.yaml +++ b/syft/pkg/cataloger/binary/testdata/config.yaml @@ -1570,8 +1570,16 @@ from-images: - version: 1.9.6 images: - - ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:1405cc613bd95b2c6edd8b2a152510ae91c7e62aea4698500d23b2145960ab9c + # note: per-platform manifest digests (not the index digest) -- the extraction step creates a + # container from the ref alone, so each platform needs its own uniquely addressable ref + - ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:0939639a1f338a9eaaa490fd38b4a7881e47a7fd1a473baf8749ce15952b55b8 platform: linux/amd64 + - ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:5fb823cc617cbfbee4b250ad5e059c23078ea63e9a7e6acb6d4d30e4456eb0be + platform: linux/arm64 + - ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:dcea8d021e15cf27f59249ee0cfd211559ef9eb60b10651c3af299499848cf8b + platform: linux/arm + - ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:fc53a604eb2d17fa0cf7a3f266d052c2f32fb97209869057d6826d1815cf622d + platform: linux/s390x paths: - /nginx-ingress-controller From 78f39ee3a5a287ff6d4c07b66f3714964b380f57 Mon Sep 17 00:00:00 2001 From: anchor Date: Thu, 20 Aug 2026 20:40:18 +0800 Subject: [PATCH 138/168] fix(javascript): strip underscore peer-dep suffixes from pnpm v5 lockfile versions (#5175) pnpm v5 lockfiles encode resolved peer dependencies as an underscore-delimited suffix of the dependency path, e.g. /acorn-jsx/5.3.2_acorn@8.8.0 or a hashed form like /webpack-cli/4.10.0_fzn43tb6bdtdxy2s3aqevve2su. Only the v6+ parenthesized form was stripped, so for v5 lockfiles the suffix leaked into reported versions and PURLs, and peer variants of the same name+version were not deduplicated. Strip the suffix on the v5 code path (version < 6.0) wherever a version is extracted: top-level dependencies values, packages keys, and per-package dependencies map values. Only values with a leading digit are stripped so link:/file:/git specifiers are unaffected. Fixes #5174 Signed-off-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com> Co-authored-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com> Co-authored-by: Cursor --- .../cataloger/javascript/parse_pnpm_lock.go | 27 +++++++ .../javascript/parse_pnpm_lock_test.go | 81 +++++++++++++++++++ .../pnpm-v5-peer-suffix/pnpm-lock.yaml | 27 +++++++ 3 files changed, 135 insertions(+) create mode 100644 syft/pkg/cataloger/javascript/testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go index 4d83ecfa6..eede2ccc8 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go @@ -89,6 +89,8 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage, return nil, fmt.Errorf("failed to unmarshal pnpm v6 lockfile: %w", err) } + isV5 := version < 6.0 + packages := make(map[string]pnpmPackage) // Direct dependencies — use sorted keys for deterministic output @@ -98,6 +100,9 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage, log.WithFields("package", name, "error", err).Trace("unable to parse pnpm dependency") continue } + if isV5 { + ver = stripPnpmV5PeerSuffix(ver) + } key := name + "@" + ver packages[key] = pnpmPackage{Name: name, Version: ver} } @@ -114,6 +119,9 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage, log.WithFields("key", key).Trace("unable to parse pnpm package key") continue } + if isV5 { + ver = stripPnpmV5PeerSuffix(ver) + } pkgKey := name + "@" + ver integrity := "" @@ -124,6 +132,9 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage, dependencies := make(map[string]string) for depName, depVersion := range sortedIter(pkgInfo.Dependencies) { var normalizedVersion = strings.SplitN(depVersion, "(", 2)[0] + if isV5 { + normalizedVersion = stripPnpmV5PeerSuffix(normalizedVersion) + } dependencies[depName] = normalizedVersion } @@ -315,6 +326,22 @@ func parseVersionField(name string, info any) (string, error) { } } +// stripPnpmV5PeerSuffix removes the underscore-delimited peer dependency suffix used by +// pnpm v5 lockfiles, e.g. "5.3.2_acorn@8.8.0" or "4.10.0_fzn43tb6bdtdxy2s3aqevve2su" -> "5.3.2" / "4.10.0". +// Lockfile v6+ encodes the same information in parentheses, which is stripped separately. +// Only values that look like a registry version (leading digit) are stripped, so that +// link:/file:/git specifiers are left untouched. +func stripPnpmV5PeerSuffix(version string) string { + idx := strings.Index(version, "_") + if idx <= 0 { + return version + } + if version[0] < '0' || version[0] > '9' { + return version + } + return version[:idx] +} + // parsePnpmPackageKey extracts the package name and version from a lockfile package key. // Handles formats like: // - /@babel/runtime/7.16.7 diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go index aca4a7c52..700603ee3 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go @@ -310,6 +310,66 @@ func TestParsePnpmV6Lock(t *testing.T) { pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) } +func TestParsePnpmLockV5PeerSuffix(t *testing.T) { + // pnpm v5 lockfiles encode resolved peer dependencies as an underscore-delimited + // suffix of the dependency path, either readable (e.g. "5.3.2_acorn@8.8.0") or + // hashed (e.g. "4.10.0_fzn43tb6bdtdxy2s3aqevve2su"). The suffix must be stripped + // from reported versions, just like the parenthesized form in v6+ lockfiles. + fixture := "testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml" + + locationSet := file.NewLocationSet(file.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ + { + Name: "acorn", + Version: "8.8.0", + PURL: "pkg:npm/acorn@8.8.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.PnpmLockEntry{ + Resolution: pkg.PnpmLockResolution{Integrity: "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w=="}, + Dependencies: map[string]string{}, + }, + }, + { + Name: "acorn-jsx", + Version: "5.3.2", + PURL: "pkg:npm/acorn-jsx@5.3.2", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.PnpmLockEntry{ + Resolution: pkg.PnpmLockResolution{Integrity: "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="}, + Dependencies: map[string]string{ + "acorn": "8.8.0", + }, + }, + }, + { + Name: "webpack-cli", + Version: "4.10.0", + PURL: "pkg:npm/webpack-cli@4.10.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.PnpmLockEntry{ + Resolution: pkg.PnpmLockResolution{Integrity: "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w=="}, + Dependencies: map[string]string{}, + }, + }, + } + expectedRelationships := []artifact.Relationship{ + { + From: expectedPkgs[0], + To: expectedPkgs[1], + Type: artifact.DependencyOfRelationship, + }, + } + adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) + pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) +} + func TestParsePnpmLockV9(t *testing.T) { var expectedRelationships []artifact.Relationship fixture := "testdata/pnpm-v9/pnpm-lock.yaml" @@ -592,6 +652,27 @@ packages: assert.Equal(t, "1.0.0", pkgs[0].Version) assert.Equal(t, "sha512-BBB", pkgs[0].Integrity, "expected last lexicographic key to win") } + + // v5 lockfile with two entries that collapse to the same key (underscore peer-dep suffixes) + lockfileV5 := []byte(` +lockfileVersion: 5.4 +packages: + /some-pkg/1.0.0_peer-b@2.0.0: + resolution: {integrity: sha512-BBB} + /some-pkg/1.0.0_peer-a@1.0.0: + resolution: {integrity: sha512-AAA} +`) + + for range 10 { + parser := &pnpmV6LockYaml{} + pkgs, err := parser.Parse(5.4, lockfileV5) + require.NoError(t, err) + require.Len(t, pkgs, 1, "expected exactly one package after key collision") + + assert.Equal(t, "some-pkg", pkgs[0].Name) + assert.Equal(t, "1.0.0", pkgs[0].Version) + assert.Equal(t, "sha512-BBB", pkgs[0].Integrity, "expected last lexicographic key to win") + } } func generateMockNpmRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { diff --git a/syft/pkg/cataloger/javascript/testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml b/syft/pkg/cataloger/javascript/testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml new file mode 100644 index 000000000..0cde2acef --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml @@ -0,0 +1,27 @@ +lockfileVersion: 5.4 + +specifiers: + acorn: ^8.8.0 + acorn-jsx: ^5.3.2 + webpack-cli: ^4.10.0 + +dependencies: + acorn: 8.8.0 + acorn-jsx: 5.3.2_acorn@8.8.0 + webpack-cli: 4.10.0_fzn43tb6bdtdxy2s3aqevve2su + +packages: + /acorn/8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + engines: {node: '>=0.4.0'} + hasBin: true + + /acorn-jsx/5.3.2_acorn@8.8.0: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.0 + + /webpack-cli/4.10.0_fzn43tb6bdtdxy2s3aqevve2su: + resolution: {integrity: sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==} From fd4796b0d838b1b31765c28179c8f16a1a02992f Mon Sep 17 00:00:00 2001 From: Sueun Cho Date: Thu, 20 Aug 2026 22:06:17 +0900 Subject: [PATCH 139/168] fix(python): keep epoch-pinned requirements in the SBOM (#5161) parsePinnedVersion rejected any version constraint containing "!" to skip the "!=" exclusion operator. PEP 440 epochs use "!" as well (e.g. "1!2.0.0"), so an exact pin like "pkg == 1!2.0.0" was treated as unparseable and returned "". parseRequirementsTxt drops a requirement whose version resolves to "", so the package never made it into the SBOM at all in either the pinned or guessed path. Check for the "!=" operator as a substring instead of the bare "!" character, which leaves the epoch separator alone. Signed-off-by: Sueun Cho --- .../cataloger/python/parse_requirements.go | 5 ++++- .../python/parse_requirements_test.go | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/python/parse_requirements.go b/syft/pkg/cataloger/python/parse_requirements.go index a7117bdee..f79737e6a 100644 --- a/syft/pkg/cataloger/python/parse_requirements.go +++ b/syft/pkg/cataloger/python/parse_requirements.go @@ -188,7 +188,10 @@ func parseVersion(version string, guessFromConstraint bool) string { func parsePinnedVersion(version string) string { version = strings.TrimSpace(version) - if strings.ContainsAny(version, "*,<>!") { + // a wildcard, range, or list of constraints is not a single pinned version. the "!=" exclusion + // operator is checked as a substring rather than by the bare "!" character, since a lone "!" + // is also the PEP 440 epoch separator (e.g. "1!2.0.0") and must not disqualify an exact pin. + if strings.ContainsAny(version, "*,<>") || strings.Contains(version, "!=") { return "" } diff --git a/syft/pkg/cataloger/python/parse_requirements_test.go b/syft/pkg/cataloger/python/parse_requirements_test.go index 735804e50..1175f7e4d 100644 --- a/syft/pkg/cataloger/python/parse_requirements_test.go +++ b/syft/pkg/cataloger/python/parse_requirements_test.go @@ -562,6 +562,26 @@ func Test_parseVersion(t *testing.T) { version: "== 1.0a1.post2.dev3+local.1", want: "1.0a1.post2.dev3+local.1", }, + { + name: "epoch", + version: "== 1!2.0.0", + want: "1!2.0.0", + }, + { + name: "epoch with all segments combined", + version: "== 1!1.0a1.post2.dev3+local.1", + want: "1!1.0a1.post2.dev3+local.1", + }, + { + name: "arbitrary equality with epoch", + version: "=== 1!2.0.0", + want: "1!2.0.0", + }, + { + name: "bare exclusion is not a pin", + version: "!= 1.1.0", + want: "", + }, { name: "resolve lowest, simple constraint", version: " >= 1.0.0 ", From 490732aa58754a7a8fef1d842d7dfb4a83300595 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:01:24 +0200 Subject: [PATCH 140/168] fix(cpe): fix generated cpe for rust packages --------- Signed-off-by: Xenira <1288524+Xenira@users.noreply.github.com> --- .../internal/cpegenerate/generate.go | 86 +++++++++++-------- .../internal/cpegenerate/generate_test.go | 19 ++++ 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate.go b/syft/pkg/cataloger/internal/cpegenerate/generate.go index 44f3606bd..097cf4e32 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate.go @@ -184,9 +184,13 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE { } func candidateTargetSw(p pkg.Package) []string { - if p.Type == pkg.WordpressPluginPkg { + switch p.Type { + case pkg.WordpressPluginPkg: return []string{"wordpress"} + case pkg.RustPkg: + return []string{"rust"} } + return []string{cpe.Any} } @@ -217,6 +221,50 @@ func candidateVendors(p pkg.Package) []string { } } + vendors = candidateVendorsByType(p, vendors) + + if p.Type == pkg.BinaryPkg && endsWithNumber(p.Name) { + // add binary package digit-suffix variations (e.g. Qt5 -> Qt) + addBinaryPackageDigitVariations(vendors) + } + + // We should no longer be generating vendor candidates with these values ["" and "*"] + // (since CPEs will match any other value) + vendors.removeByValue("") + vendors.removeByValue("*") + + // try swapping hyphens for underscores, vice versa, and removing separators altogether + addDelimiterVariations(vendors) + + // rust vendor name needs to be added after the `addDelimiterVariations` call as `-project` suffix is used otherwise + if p.Language == pkg.Rust { + vendors.addValue(p.Name + "_project") + } + + // generate sub-selections of each candidate based on separators (e.g. jenkins-ci -> [jenkins, jenkins-ci]) + addAllSubSelections(vendors) + + // add more candidates based on the package info for each vendor candidate + for _, vendor := range vendors.uniqueValues() { + vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, p.Type, p.Name, vendor)...) + } + + // remove known mis + vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, p.Type, p.Name)...) + + uniqueVendors := vendors.uniqueValues() + + // if any known vendor was detected, pick that one. + for _, vendor := range uniqueVendors { + if knownVendors.Has(vendor) { + return []string{vendor} + } + } + + return uniqueVendors +} + +func candidateVendorsByType(p pkg.Package, vendors fieldCandidateSet) fieldCandidateSet { switch p.Metadata.(type) { case pkg.DotnetDepsEntry, pkg.DotnetPackagesLockEntry, pkg.DotnetPortableExecutableEntry: vendors.clear() @@ -240,41 +288,7 @@ func candidateVendors(p pkg.Package) []string { vendors.clear() vendors.union(candidateVendorsForWordpressPlugin(p)) } - - if p.Type == pkg.BinaryPkg && endsWithNumber(p.Name) { - // add binary package digit-suffix variations (e.g. Qt5 -> Qt) - addBinaryPackageDigitVariations(vendors) - } - - // We should no longer be generating vendor candidates with these values ["" and "*"] - // (since CPEs will match any other value) - vendors.removeByValue("") - vendors.removeByValue("*") - - // try swapping hyphens for underscores, vice versa, and removing separators altogether - addDelimiterVariations(vendors) - - // generate sub-selections of each candidate based on separators (e.g. jenkins-ci -> [jenkins, jenkins-ci]) - addAllSubSelections(vendors) - - // add more candidates based on the package info for each vendor candidate - for _, vendor := range vendors.uniqueValues() { - vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, p.Type, p.Name, vendor)...) - } - - // remove known mis - vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, p.Type, p.Name)...) - - uniqueVendors := vendors.uniqueValues() - - // if any known vendor was detected, pick that one. - for _, vendor := range uniqueVendors { - if knownVendors.Has(vendor) { - return []string{vendor} - } - } - - return uniqueVendors + return vendors } func candidateProducts(p pkg.Package) []string { diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go index 890d60170..704468780 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go @@ -855,6 +855,25 @@ func TestGeneratePackageCPEs(t *testing.T) { }, expected: []string{}, }, + { + name: "rust package", + p: pkg.Package{ + Name: "rust-package", + Version: "0.5.0", + Type: pkg.RustPkg, + Language: pkg.Rust, + }, + expected: []string{ + "cpe:2.3:a:rust-package_project:rust-package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust-package_project:rust_package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust-package:rust-package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust-package:rust_package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust:rust-package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust:rust_package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust_package:rust-package:0.5.0:*:*:*:*:rust:*:*", + "cpe:2.3:a:rust_package:rust_package:0.5.0:*:*:*:*:rust:*:*", + }, + }, } for _, test := range tests { From 168eb64235165003299c85090d3265a674ffc67f Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 20 Aug 2026 16:01:52 -0400 Subject: [PATCH 141/168] fix(javascript): pass a yaml node to the pnpm v5 collision test (#5203) #5188 changed `pnpmV6LockYaml.Parse` to take a `*yaml.Node` and #5175 added this test against the old `[]byte` signature. both were green on their own branches, so main does not compile. the v9 and v6 cases in the same test already go through the `yamlDocument` helper, this is just the v5 one that was missed. Signed-off-by: Alex Goodman --- syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go index 700603ee3..35c8e0555 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go @@ -654,18 +654,18 @@ packages: } // v5 lockfile with two entries that collapse to the same key (underscore peer-dep suffixes) - lockfileV5 := []byte(` + lockfileV5 := ` lockfileVersion: 5.4 packages: /some-pkg/1.0.0_peer-b@2.0.0: resolution: {integrity: sha512-BBB} /some-pkg/1.0.0_peer-a@1.0.0: resolution: {integrity: sha512-AAA} -`) +` for range 10 { parser := &pnpmV6LockYaml{} - pkgs, err := parser.Parse(5.4, lockfileV5) + pkgs, err := parser.Parse(5.4, yamlDocument(t, lockfileV5)) require.NoError(t, err) require.Len(t, pkgs, 1, "expected exactly one package after key collision") From 236755a6b11c5fcab8a0a43759dcec30895c4c56 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:02:14 -0400 Subject: [PATCH 142/168] fix(binary): gzip classifier & GNU gzip identity string (#5202) --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- syft/pkg/cataloger/binary/classifiers.go | 5 +++++ .../binary/testdata/classifiers/negative/gzip | Bin 0 -> 148 bytes .../snippets/gzip/1.12/linux-amd64/gzip | Bin 346 -> 376 bytes 3 files changed, 5 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/negative/gzip diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 78f6e18ed..26fd9ce7f 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -899,6 +899,11 @@ func DefaultClassifiers() []binutils.Classifier { Class: "gzip-binary", FileGlob: "**/gzip", EvidenceMatcher: m.FileContentsVersionMatcher( + // GNU gzip keeps the version as a bare NUL-delimited token, so require an identifying + // string from the program as well. Without it any binary that happens to be named gzip + // (such as the busybox multicall binary behind a gzip applet symlink) would have an + // arbitrary NUL-delimited "N.N" token reported as a GNU gzip version. + `%s: %s: not in gzip format`, `\x00(?P[0-9]+\.[0-9]+)\x00`, ), Package: "gzip", diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/negative/gzip b/syft/pkg/cataloger/binary/testdata/classifiers/negative/gzip new file mode 100644 index 0000000000000000000000000000000000000000..e6c2a242f2355407ee2ab6e560c98fc14f856045 GIT binary patch literal 148 zcmYk!u@1r@7)9aEJjG31+MxuhiAl%0>rx*8S`vtVp%F0h_7Yu8&ib7Ld`MZ2-5+72z1T=~t*cPfmC~r3x6)Qdw9;7P z8fvK~RVfl(V_S>b>93ORPDXuTD ugxfASbJIMpc~9oGoY{Z*)66NPabM!h8V}jHH;D8>k>MHT7Tm1wr{_PgB1UQe delta 176 zcmaLEF%E(-6adib$N`+YkmzFQFD<{U9Kqe>(|&{m3bFMb8gJ)G9KC_>efOXJ^Kf-o z%p7ldY^8##NO9UW&X Date: Fri, 21 Aug 2026 06:16:45 +1000 Subject: [PATCH 143/168] fix(file-resolver): don't abort scan on inaccessible symlink target (#5170) Signed-off-by: addielarue --- .../fileresolver/directory_indexer.go | 8 +++++ .../fileresolver/directory_indexer_test.go | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/syft/internal/fileresolver/directory_indexer.go b/syft/internal/fileresolver/directory_indexer.go index 4ae58702c..e748a5015 100644 --- a/syft/internal/fileresolver/directory_indexer.go +++ b/syft/internal/fileresolver/directory_indexer.go @@ -126,6 +126,14 @@ func (r *directoryIndexer) indexTree(root string, stager *progress.AtomicStage) shouldIndexFullTree, err := isRealPath(root) if err != nil { + // a symlink can resolve into a directory we don't have permission to traverse (or into a path + // that no longer exists). Like any other inaccessible path, this should be recorded and skipped + // with a warning rather than aborting the entire scan (see #3286). + var pathErr *os.PathError + if errors.As(err, &pathErr) { + r.isFileAccessErr(root, err) + return nil, nil + } return nil, err } diff --git a/syft/internal/fileresolver/directory_indexer_test.go b/syft/internal/fileresolver/directory_indexer_test.go index ace9aa9ce..d24a86201 100644 --- a/syft/internal/fileresolver/directory_indexer_test.go +++ b/syft/internal/fileresolver/directory_indexer_test.go @@ -286,6 +286,42 @@ func TestDirectoryIndexer_index_survive_badSymlink(t *testing.T) { require.NoError(t, err) } +func TestDirectoryIndexer_index_survive_inaccessibleSymlinkTarget(t *testing.T) { + // a symlink that resolves into a directory we don't have permission to traverse should be skipped + // with a warning like any other inaccessible path, not abort the entire scan (see #3286). + if os.Geteuid() == 0 { + t.Skip("cannot run as root: root bypasses the directory permissions that trigger the bug") + } + + // resolve any symlinks in the temp path (e.g. macOS /var -> /private/var) so the indexer root is a + // real path -- otherwise indexing takes the symlinked-branch code path and never reaches the bug. + root, err := filepath.EvalSymlinks(t.TempDir()) + require.NoError(t, err) + + // root/ + // ├── restricted/ (chmod 0000 -- cannot be traversed) + // │ └── nested/ + // │ └── target.txt + // └── link -> root/restricted/nested/target.txt + nested := filepath.Join(root, "restricted", "nested") + require.NoError(t, os.MkdirAll(nested, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(nested, "target.txt"), []byte("hello"), 0o644)) + require.NoError(t, os.Symlink(filepath.Join(root, "restricted", "nested", "target.txt"), filepath.Join(root, "link"))) + + restricted := filepath.Join(root, "restricted") + require.NoError(t, os.Chmod(restricted, 0o000)) + // restore permissions so t.TempDir cleanup can remove the tree + t.Cleanup(func() { _ = os.Chmod(restricted, 0o755) }) + + // base is empty, matching a plain `syft ` invocation (the reporter's scenario) + indexer := newDirectoryIndexer(root, "") + _, _, err = indexer.build() + require.NoError(t, err) + + // the inaccessible path should be recorded rather than silently dropped + require.NotEmpty(t, indexer.errPaths) +} + func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) { var observedPaths []string pathObserver := func(_, p string, _ os.FileInfo, _ error) error { From 39c9c690df90d56b2a45258a081845cbb5a44cb4 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 20 Aug 2026 16:17:41 -0400 Subject: [PATCH 144/168] fix: move image hardlink handling upstream during image indexing (#5196) * chore(deps): pin stereoscope to the hardlink-inode-adoption branch temporary pin to anchore/stereoscope#670 so the hardlink changes can be exercised end to end. needs re-pinning to a release tag before merge. pulls transitive bumps along with it: docker/cli, docker/go-connections and gabriel-vasile/mimetype. Signed-off-by: Alex Goodman * fix: drop the image resolver hardlink workaround stereoscope now adopts a hardlink's target at index time, so a hardlinked name arrives already described as a regular file with the target's size, mime type and content. the resolver-side fix from #5029 sat on top of that doing the same job a second time, gated on a file type that no longer shows up, so both `resolveHardLinkTarget` implementations and the extra tree walk they cost on every `FilesByPath` are gone. `file.NewVirtualLocationFromImage` stays as-is. it is exported and syft is v1, so it keeps working for anyone using it, it just has no callers in syft now. this picks up `FilesByMIMEType`, which #5029 explicitly could not fix, so hardlinked names now reach mime-driven catalogers. on images built around multi-call binaries (busybox and friends) that is a lot more file and executable entries than before; distro and toolchain images move by about one entry. one shape regresses: a hardlink stereoscope could not adopt (a link name that is absent, empty, names a directory, names another un-adopted link, or points into a lower layer) keeps `TypeHardLink`, and syft goes back to collapsing it onto its target's path. no mainstream builder emits the cross-layer case. Signed-off-by: Alex Goodman * bump stereoscope to main Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- go.mod | 8 +-- go.sum | 16 ++--- syft/file/location.go | 9 +-- syft/format/syftjson/to_syft_model.go | 2 + .../container_image_all_layers.go | 53 ---------------- .../fileresolver/container_image_squash.go | 60 ++----------------- .../container_image_squash_test.go | 18 ++++++ 7 files changed, 38 insertions(+), 128 deletions(-) diff --git a/go.mod b/go.mod index c01e26d99..e29fc2d49 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/anchore/go-sync v0.1.1 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.2.0 - github.com/anchore/stereoscope v0.3.0 + github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef @@ -183,9 +183,9 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v29.6.2+incompatible // indirect + github.com/docker/cli v29.7.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect - github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-connections v0.8.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/emirpasic/gods v1.18.1 // indirect @@ -196,7 +196,7 @@ require ( github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/gkampitakis/ciinfo v0.3.4 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect diff --git a/go.sum b/go.sum index 696231d2b..98738ec8a 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= -github.com/anchore/stereoscope v0.3.0 h1:6mGGRxm4zRKj/jNiSM7g6XGwWihrnkQRJj11hcULDqY= -github.com/anchore/stereoscope v0.3.0/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI= +github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf h1:eTQTL06tadOz+V4ubhXOlc5oKQ11cXbyo/clnxlcsnU= +github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf/go.mod h1:cWQ8fLFblnkJ0c3BSFY+BwXe4DcJDdysP/gnoWHVb7s= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -339,12 +339,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= -github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= +github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= +github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= @@ -403,8 +403,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= -github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ= github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= diff --git a/syft/file/location.go b/syft/file/location.go index f5e1aa51c..76678c40f 100644 --- a/syft/file/location.go +++ b/syft/file/location.go @@ -153,14 +153,9 @@ func NewLocationFromImage(accessPath string, ref file.Reference, img *image.Imag } // NewVirtualLocationFromImage creates a new Location surfaced at realPath but whose contents and metadata are -// described by the given reference. This is used to present a hardlink as the underlying type it points to (at the -// hardlink's own path) so that image results are in parity with directory results, which cannot distinguish a -// hardlink from a regular file. Note that RealPath (the hardlink's own path) intentionally differs from -// ref.RealPath (the target's path); callers must not assume the two agree for these locations. +// described by the given reference. Note that RealPath may differ from ref.RealPath; callers must not assume the two +// agree for these locations. The FileSystemID comes from the given ref's layer. func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location { - // the FileSystemID comes from the target ref's layer. a hardlink and its target are always materialized in the - // same layer tar (a tar hardlink entry can only reference a file within the same archive), so the target's layer - // digest is also the hardlink path's layer. layer := img.FileCatalog.Layer(ref) return Location{ LocationData: LocationData{ diff --git a/syft/format/syftjson/to_syft_model.go b/syft/format/syftjson/to_syft_model.go index ee227b00c..651f8bba9 100644 --- a/syft/format/syftjson/to_syft_model.go +++ b/syft/format/syftjson/to_syft_model.go @@ -175,6 +175,8 @@ func toSyftFileType(ty string) stereoscopeFile.Type { case "SymbolicLink": return stereoscopeFile.TypeSymLink case "HardLink": + // kept for reading SBOMs written before stereoscope adopted hardlinks at index time (they are emitted as + // regular files now, except for malformed link entries) return stereoscopeFile.TypeHardLink case "Directory": return stereoscopeFile.TypeDirectory diff --git a/syft/internal/fileresolver/container_image_all_layers.go b/syft/internal/fileresolver/container_image_all_layers.go index 20e8136bc..1a3151e8c 100644 --- a/syft/internal/fileresolver/container_image_all_layers.go +++ b/syft/internal/fileresolver/container_image_all_layers.go @@ -56,18 +56,6 @@ func (r *ContainerImageAllLayers) HasPath(path string) bool { func (r *ContainerImageAllLayers) locationsByRef(ref stereoscopeFile.Reference, accessPath string, uniqueFileIDs stereoscopeFile.ReferenceSet, layerPos int) ([]file.Location, error) { uniqueLocations := make([]file.Location, 0) - // if the access path is itself a hardlink, surface it as the underlying type it points to, at its own path - // (bound to the target's content), so that image results are in parity with directory results (which cannot - // tell a hardlink from a regular file). the path-based lookup is required because the search that produced ref - // already followed the basename link, collapsing the hardlink onto its target. - if ownRef, targetRef, ok := r.hardLinkAtPath(accessPath, r.layers[layerPos]); ok { - if !uniqueFileIDs.Contains(ownRef) { - uniqueFileIDs.Add(ownRef) - uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), accessPath, targetRef, r.img)) - } - return uniqueLocations, nil - } - // since there is potentially considerable work for each symlink/hardlink that needs to be resolved, let's check to see if this is a symlink/hardlink first entry, err := r.img.FileCatalog.Get(ref) if err != nil { @@ -95,25 +83,6 @@ func (r *ContainerImageAllLayers) locationsByRef(ref stereoscopeFile.Reference, return uniqueLocations, nil } -// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a -// hardlink within the given layer. ok is false when path does not exist there or is not a hardlink. The lookup does -// not follow the basename link so that the hardlink's own path is preserved. This adds a tree walk per -// matched ref (path x layer); if it shows up in profiles, fold the hardlink check into the existing search resolution. -func (r *ContainerImageAllLayers) hardLinkAtPath(path string, layerIdx int) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) { - var own stereoscopeFile.Reference - // use the squashed-to-layer view (not just this layer's additions) so a hardlink added in a lower layer is still - // detected when the path is searched at a higher layer; otherwise it would collapse onto its target there. - exists, resolution, err := r.img.Layers[layerIdx].SquashedTree.File(stereoscopeFile.Path(path)) - if err != nil || !exists || !resolution.HasReference() { - return own, own, false - } - target, ok := r.resolveHardLinkTarget(*resolution.Reference, layerIdx) - if !ok { - return own, own, false - } - return *resolution.Reference, target, true -} - // FilesByPath returns all file.References that match the given paths from any layer in the image. func (r *ContainerImageAllLayers) FilesByPath(paths ...string) ([]file.Location, error) { uniqueFileIDs := stereoscopeFile.NewFileReferenceSet() @@ -284,11 +253,6 @@ func (r *ContainerImageAllLayers) AllLocations(ctx context.Context) <-chan file. tree := r.img.Layers[layerIdx].Tree for _, ref := range tree.AllFiles(stereoscopeFile.AllTypes()...) { l := file.NewLocationFromImage(string(ref.RealPath), ref, r.img) - // surface a hardlink as the underlying type it points to (at its own path) so image results match - // directory results, which cannot distinguish a hardlink from a regular file. - if targetRef, ok := r.resolveHardLinkTarget(ref, layerIdx); ok { - l = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img) - } r.annotateLocation(&l) select { case <-ctx.Done(): @@ -306,21 +270,6 @@ func (r *ContainerImageAllLayers) FileMetadataByLocation(location file.Location) return fileMetadataByLocation(r.img, location) } -// resolveHardLinkTarget returns the reference of a hardlink's underlying target (resolved relative to the given layer) -// when ref is a hardlink; ok is false otherwise. No resolution is performed for non-hardlinks (symlinks keep their -// existing resolution semantics). -func (r *ContainerImageAllLayers) resolveHardLinkTarget(ref stereoscopeFile.Reference, layerIdx int) (stereoscopeFile.Reference, bool) { - metadata, err := r.img.FileCatalog.Get(ref) - if err != nil || metadata.Type != stereoscopeFile.TypeHardLink { - return ref, false - } - resolved, err := r.img.ResolveLinkByLayerSquash(ref, layerIdx) - if err != nil || !resolved.HasReference() { - return ref, false - } - return *resolved.Reference, true -} - func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) { if !r.markVisibility || l == nil { return @@ -345,8 +294,6 @@ func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) { } // pathResolvesToRef reports whether the given path in the squashed tree resolves to the given reference. -// SearchByPath always follows basename links, so a hardlink surfaced at its own path (whose reference is its -// target) still resolves to the target here and is correctly considered visible. func (r *ContainerImageAllLayers) pathResolvesToRef(path string, target stereoscopeFile.Reference) bool { ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.DoNotFollowDeadBasenameLinks) return err == nil && ref.HasReference() && ref.ID() == target.ID() diff --git a/syft/internal/fileresolver/container_image_squash.go b/syft/internal/fileresolver/container_image_squash.go index ef7122af6..efcd2e8e4 100644 --- a/syft/internal/fileresolver/container_image_squash.go +++ b/syft/internal/fileresolver/container_image_squash.go @@ -40,17 +40,6 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er uniqueLocations := make([]file.Location, 0) for _, path := range paths { - // if the requested path is itself a hardlink, surface it at its own path (bound to its target's content) - // rather than collapsing it onto the target's path, so that image results are in parity with directory - // results (which cannot tell a hardlink from a regular file). - if ownRef, targetRef, ok := r.hardLinkAtPath(path); ok { - if !uniqueFileIDs.Contains(ownRef) { - uniqueFileIDs.Add(ownRef) - uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), path, targetRef, r.img)) - } - continue - } - ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.FollowBasenameLinks) if err != nil { return nil, err @@ -93,7 +82,7 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er // //nolint:gocognit func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location, error) { - uniqueCoordinates := file.NewCoordinateSet() + uniqueFileIDs := stereoscopeFile.NewFileReferenceSet() uniqueLocations := make([]file.Location, 0) for _, pattern := range patterns { @@ -127,14 +116,10 @@ func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location, return nil, fmt.Errorf("failed to find files by path (result=%+v): %w", result, err) } for _, resolvedLocation := range resolvedLocations { - // dedup on the surfaced coordinate rather than the underlying reference: distinct hardlinks share a - // single target reference but each has its own real path, so a reference-based dedup would collapse - // them back onto one entry (the exact behavior this parity fix removes). symlink resolutions keep - // their target's real path, so they still collapse as before. - if uniqueCoordinates.Contains(resolvedLocation.Coordinates) { + if uniqueFileIDs.Contains(resolvedLocation.Reference()) { continue } - uniqueCoordinates.Add(resolvedLocation.Coordinates) + uniqueFileIDs.Add(resolvedLocation.Reference()) uniqueLocations = append(uniqueLocations, resolvedLocation) } } @@ -194,16 +179,10 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc go func() { defer close(results) for _, ref := range r.img.SquashedTree().AllFiles(stereoscopeFile.AllTypes()...) { - loc := file.NewLocationFromImage(string(ref.RealPath), ref, r.img) - // surface a hardlink as the underlying type it points to (at its own path) so image results match - // directory results, which cannot distinguish a hardlink from a regular file. - if targetRef, ok := r.resolveHardLinkTarget(ref); ok { - loc = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img) - } select { case <-ctx.Done(): return - case results <- loc: + case results <- file.NewLocationFromImage(string(ref.RealPath), ref, r.img): continue } } @@ -211,37 +190,6 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc return results } -// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a -// hardlink. ok is false when path does not exist or is not a hardlink. The lookup does not follow the basename link so -// that the hardlink's own path is preserved. This adds a tree walk per FilesByPath path; if it shows up in -// profiles, fold the hardlink check into the existing SearchByPath resolution. -func (r *ContainerImageSquash) hardLinkAtPath(path string) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) { - var own stereoscopeFile.Reference - exists, resolution, err := r.img.SquashedTree().File(stereoscopeFile.Path(path)) - if err != nil || !exists || !resolution.HasReference() { - return own, own, false - } - target, ok := r.resolveHardLinkTarget(*resolution.Reference) - if !ok { - return own, own, false - } - return *resolution.Reference, target, true -} - -// resolveHardLinkTarget returns the reference of a hardlink's underlying target when ref is a hardlink; ok is false -// otherwise. No resolution is performed for non-hardlinks (symlinks keep their existing resolution semantics). -func (r *ContainerImageSquash) resolveHardLinkTarget(ref stereoscopeFile.Reference) (stereoscopeFile.Reference, bool) { - metadata, err := r.img.FileCatalog.Get(ref) - if err != nil || metadata.Type != stereoscopeFile.TypeHardLink { - return ref, false - } - resolved, err := r.img.ResolveLinkByImageSquash(ref) - if err != nil || !resolved.HasReference() { - return ref, false - } - return *resolved.Reference, true -} - func (r *ContainerImageSquash) FilesByMIMEType(types ...string) ([]file.Location, error) { refs, err := r.img.SquashedSearchContext.SearchByMIMEType(types...) if err != nil { diff --git a/syft/internal/fileresolver/container_image_squash_test.go b/syft/internal/fileresolver/container_image_squash_test.go index 82b0b87fb..b1ceaca0d 100644 --- a/syft/internal/fileresolver/container_image_squash_test.go +++ b/syft/internal/fileresolver/container_image_squash_test.go @@ -642,6 +642,24 @@ func TestImageResolvers_Hardlinks(t *testing.T) { } }) + t.Run("FilesByMIMEType surfaces every hardlink name", func(t *testing.T) { + // this is the surface #5029 could not fix and stereoscope's index-time adoption does: MIME-driven + // catalogers see each name. it also pins that the names are separately addressable (distinct + // references), since this resolver dedups on reference and would return one entry if they shared it. + locs, err := resolver.FilesByMIMEType("text/plain") + require.NoError(t, err) + + byPath := map[string]file.Location{} + for _, loc := range locs { + byPath[loc.RealPath] = loc + } + for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} { + loc, ok := byPath[path] + require.Truef(t, ok, "expected MIME search to surface %s (got %v)", path, byPath) + assertHardlink(t, resolver, path, loc) + } + }) + t.Run("AllLocations includes every hardlink path as a regular file exactly once", func(t *testing.T) { counts := map[string]int{} locsByPath := map[string]file.Location{} From e820f2d21a309d1d34434588de1f692e29370cce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:29:25 +0000 Subject: [PATCH 145/168] chore(deps): bump golang.org/x/tools from 0.48.0 to 0.49.0 (#5205) Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.48.0 to 0.49.0. - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.48.0...v0.49.0) --- updated-dependencies: - dependency-name: golang.org/x/tools dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index e29fc2d49..b653e319d 100644 --- a/go.mod +++ b/go.mod @@ -93,10 +93,10 @@ require ( go.uber.org/goleak v1.3.0 go.yaml.in/yaml/v3 v3.0.5 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f - golang.org/x/mod v0.38.0 - golang.org/x/net v0.57.0 + golang.org/x/mod v0.39.0 + golang.org/x/net v0.58.0 golang.org/x/time v0.15.0 - golang.org/x/tools v0.48.0 + golang.org/x/tools v0.49.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.56.0 ) @@ -309,12 +309,12 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.54.0 // indirect + golang.org/x/crypto v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/api v0.271.0 // indirect diff --git a/go.sum b/go.sum index 98738ec8a..f6ffbab52 100644 --- a/go.sum +++ b/go.sum @@ -1041,8 +1041,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1082,8 +1082,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1128,8 +1128,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1254,8 +1254,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1316,8 +1316,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 766907e6ed673a80deac3588c0557c14d468be71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:29:57 +0000 Subject: [PATCH 146/168] chore(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.0 (#5206) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 ++---- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index b653e319d..159f6a36f 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,7 @@ require ( github.com/spdx/tools-golang v0.6.0-rc4 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.0 github.com/ulikunitz/xz v0.5.16 github.com/vbatts/go-mtree v0.7.0 github.com/vifraa/gopom v1.0.0 @@ -182,7 +182,6 @@ require ( github.com/containerd/ttrpc v1.2.8 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v29.7.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.8.1 // indirect @@ -265,7 +264,6 @@ require ( github.com/pkg/profile v1.7.0 // indirect github.com/pkg/xattr v0.4.12 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -282,7 +280,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/sif/v2 v2.24.1 // indirect github.com/sylabs/squashfs v1.0.6 // indirect diff --git a/go.sum b/go.sum index f6ffbab52..a98fc085c 100644 --- a/go.sum +++ b/go.sum @@ -908,8 +908,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -920,8 +920,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= From 29edf900381a9b7e2fbce296c03fd02c1331fe88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:50:46 +0000 Subject: [PATCH 147/168] chore(deps): bump github.com/hashicorp/go-getter from 1.8.6 to 1.8.8 (#5207) Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.8.6 to 1.8.8. - [Release notes](https://github.com/hashicorp/go-getter/releases) - [Changelog](https://github.com/hashicorp/go-getter/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.6...v1.8.8) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-getter dependency-version: 1.8.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 89 +++++++++++++------------ go.sum | 200 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 144 insertions(+), 145 deletions(-) diff --git a/go.mod b/go.mod index 159f6a36f..d1ffdb105 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( github.com/gookit/color v1.6.1 github.com/gpustack/gguf-parser-go v0.25.0 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/go-getter v1.8.6 + github.com/hashicorp/go-getter v1.8.8 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/hcl/v2 v2.24.0 github.com/iancoleman/strcase v0.3.0 @@ -108,19 +108,19 @@ require ( ) require ( - cel.dev/expr v0.25.1 // indirect + cel.dev/expr v0.25.2 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.22.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/monitoring v1.24.3 // indirect - cloud.google.com/go/storage v1.61.3 // indirect + cloud.google.com/go/iam v1.11.0 // indirect + cloud.google.com/go/monitoring v1.29.0 // indirect + cloud.google.com/go/storage v1.64.0 // indirect dario.cat/mergo v1.0.2 // indirect github.com/DataDog/zstd v1.5.5 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect github.com/Microsoft/hcsshim v0.15.0-rc.1 // indirect @@ -133,25 +133,24 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect - github.com/aws/smithy-go v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.35 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.34 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.36 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.106.5 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 // indirect + github.com/aws/smithy-go v1.27.6 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect @@ -191,7 +190,7 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -206,11 +205,11 @@ require ( github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect - github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect + github.com/googleapis/gax-go/v2 v2.23.0 // indirect + github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/henvic/httpretty v0.1.4 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -279,7 +278,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/sif/v2 v2.24.1 // indirect @@ -297,14 +296,14 @@ require ( github.com/zclconf/go-cty v1.16.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/crypto v0.55.0 // indirect @@ -315,11 +314,11 @@ require ( golang.org/x/text v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.17.0 // indirect - google.golang.org/api v0.271.0 // indirect - google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/api v0.292.0 // indirect + google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/grpc v1.83.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/warnings.v0 v0.1.2 // indirect modernc.org/libc v1.74.4 // indirect diff --git a/go.sum b/go.sum index a98fc085c..0e68e410f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -31,8 +31,8 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ= +cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -46,14 +46,14 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCB cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= -cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= -cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= -cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= +cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= +cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k= +cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= +cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= +cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= +cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY= +cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -63,10 +63,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= -cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= -cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= -cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg= +cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ= +cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= +cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -82,14 +82,14 @@ github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByX github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= @@ -169,44 +169,42 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= -github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= -github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= -github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.43.4 h1:b9FTvbRwy+JCsfp2Wp6wV/KbOx3Aj7nkoFb2cRX0IhE= +github.com/aws/aws-sdk-go-v2 v1.43.4/go.mod h1:70vwSy16txshwG+g55WkpgPKDIByzHI8ccBsOteo3bQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16 h1:aiuaKlDweRC5qExJondpWjOgyzMHpofpwspGXUtwn4c= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16/go.mod h1:nG/LOlmox9BDe9HvQnXWzgcK8uKbgBMZ/Hp5pVt/21I= +github.com/aws/aws-sdk-go-v2/config v1.32.35 h1:UEzXuET8E42lxBPijuACu/tEK7v5lFPlk0Q+GT5WD9E= +github.com/aws/aws-sdk-go-v2/config v1.32.35/go.mod h1:KaMtJpFa2JlL2BStjjHQVwQpzZEmw+ND/EgVrfFoo2g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.34 h1:y6GkSmcv5myd1ngrYbGmiLlwQqB6TQhOuN/tbSSuWDY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.34/go.mod h1:w3dTcnDVoQIewjo7JG45hduAToikiIFLC4FIO7fndvw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35 h1:+S7kbJoLDDQ5tE+lHrUBgMkzC8NLgsaioS2F3dVoFAE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35/go.mod h1:Ak7xXviIARfFdNUJ9Etb0bdVDt/KAvKjMGJVLWXDzik= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35 h1:kzVuGlatQtYinwBJEEyLAbggepCoavosiaHHX9+fD+c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35/go.mod h1:0yLx0yEI+SfqeJMPvOtIEFoZbiQYXMGszBueiutQyaI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35 h1:WK6CjihTuLisCjSKKbildJ79sGZZgbBz3iNa7VsKIhU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35/go.mod h1:KYleN57luLoe97R7vTnx8PMcVrr9gAcRECtOjl91DNg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36 h1:jbGY4CXLzZElOXgGsexlC3Hi+3YM0rSmk4opFXKqg/k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36/go.mod h1:uBu/9aKsS/UQGc72RAt3y54kjgYQxmhut8ZD2dXCDNE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 h1:JJLBQxwY+AFwuPAi5ivGc1ChnTdUt4cXMv7e76m2c/Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15/go.mod h1:lQknBIe78MVL0cQOQDlag8KGflMbMEVFx9mB6O8ENvk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.28 h1:Q1TF1J9jVD+vFo0LzNnmNdQ9EAt52TS+MQlq9Ir+Yxo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.28/go.mod h1:4KqXXC/p1hrotmouDFbrRoWaLy962b9PMUReCG6+uWo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35 h1:BBEElKh4a+rKshvjrfpajTe9CbpZvrbb4Jkg2PB7RzA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35/go.mod h1:zaZk983w//8beSruBVec/mr4CmDwgZitW/qzGhAAX0g= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.36 h1:EUIwBoN+q7UmhAejxgD27APiRjh1vwCFo53gSqdT0BM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.36/go.mod h1:6u00gmlTGR6W0b2k9NBrld7MnOEmf1Spqx0VVt6AqyE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.5 h1:HpN6GgZ3T8pSvRp81ZsgumNjlvRsa+9M0ZL2o6W4uLY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.5/go.mod h1:5FTZoQxhmLEiCAtYVk6V+t0iS/B5yGZVLZ3Wq5FDJZI= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 h1:cOJELVNrq5Q3Udry2GLuHUM7MhwpeaQRdYaoa6GI/yI= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.4/go.mod h1:f4LxzKBtaTxD7xh3PiVg3CE1tchQemfmghaJr+NbK2c= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.4 h1:AMW7a7S8iQaHjBYZdU3PCq4GKRPijTPRAc7e6XtEThY= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.4/go.mod h1:QQNsFV1DVXoXcZt18FS8lI8rtUrlDyAuWZLQ5shunv4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4 h1:AsbZcJAQPRmHDJG8K1N0pof/1zPWjVT8TFlTWuGLSvo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4/go.mod h1:6imqztH0//t0mKbl6yWl7swSEl7F/w32oAmqB3vP1ag= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 h1:w/AryDYMjSUANSQ2uoZxJovUsMTwWJNTv3IMex30Y+4= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.4/go.mod h1:WeBiAa67azG7Su9Vf+ChGDBLiAozJCXzdjXiPBUwtbc= +github.com/aws/smithy-go v1.27.6 h1:0zjT8jgK3jbrTT7JJ3EE6JsMhX8JTrZ+f1sEndYDXrA= +github.com/aws/smithy-go v1.27.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= @@ -389,8 +387,8 @@ github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrE github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= @@ -545,14 +543,14 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4= +github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= +github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= @@ -561,8 +559,8 @@ github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmty github.com/gpustack/gguf-parser-go v0.25.0 h1:1AMBhMKtI24nTtn588Bq53FqNiOvEw1x9Nb4HbRrThs= github.com/gpustack/gguf-parser-go v0.25.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk= -github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -572,8 +570,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY= -github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= +github.com/hashicorp/go-getter v1.8.8 h1:sRakhf+EH6s0LZLO2IgBwZ6hAFp+fZOZMNREwVELV80= +github.com/hashicorp/go-getter v1.8.8/go.mod h1:fqFlibKpwfns/s4oljLB3upJspyfFLQhS7031PlfUDc= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -589,8 +587,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -902,8 +900,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4= +github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -998,24 +996,26 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -1358,8 +1358,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= -google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= -google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= +google.golang.org/api v0.292.0 h1:Ewiwo/GTtiaPZSNAZQUcWLh8AYDEoPmIXyJfeoTSMHU= +google.golang.org/api v0.292.0/go.mod h1:07kjmMnFGm2RQuCza2EZM/5N68G/fVvFb1xKjWqoFA0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1430,12 +1430,12 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 h1:YJjbgu+dkp5kUJLfpMyCLfBIWZb/FcJyuLeo1gVBOuo= +google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1463,8 +1463,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From 34ef7dcbfe9d6949fefdb2f0cef4c748ecaa2b64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:50:49 +0000 Subject: [PATCH 148/168] chore(deps): bump golang.org/x/mod from 0.39.0 to 0.40.0 (#5208) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.39.0 to 0.40.0. - [Commits](https://github.com/golang/mod/compare/v0.39.0...v0.40.0) --- updated-dependencies: - dependency-name: golang.org/x/mod dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d1ffdb105..36dc71f64 100644 --- a/go.mod +++ b/go.mod @@ -93,7 +93,7 @@ require ( go.uber.org/goleak v1.3.0 go.yaml.in/yaml/v3 v3.0.5 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f - golang.org/x/mod v0.39.0 + golang.org/x/mod v0.40.0 golang.org/x/net v0.58.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.49.0 diff --git a/go.sum b/go.sum index 0e68e410f..7e280ae90 100644 --- a/go.sum +++ b/go.sum @@ -1082,8 +1082,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= -golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= From 93cf89363b2612d3bfec7a65601c68f90a21ffbb Mon Sep 17 00:00:00 2001 From: Sueun Cho Date: Sat, 22 Aug 2026 03:47:23 +0900 Subject: [PATCH 149/168] fix(rpm): keep the epoch when parsing RPM manifest packages (#5201) --------- Signed-off-by: Sueun Cho Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- syft/pkg/cataloger/redhat/package.go | 11 +-- .../redhat/parse_rpm_manifest_test.go | 92 +++++++++++++++++++ .../redhat/testdata/container-manifest-2 | 1 + 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/syft/pkg/cataloger/redhat/package.go b/syft/pkg/cataloger/redhat/package.go index fc2da4fd1..1fcbeaf1b 100644 --- a/syft/pkg/cataloger/redhat/package.go +++ b/syft/pkg/cataloger/redhat/package.go @@ -59,17 +59,14 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmDBEntry, error) { version := versionParts[0] release := versionParts[1] - converted, err := strconv.Atoi(parts[8]) + // EPOCH is "(none)" when the package has no epoch; EPOCHNUM is the numeric form ("0" in that case) var epoch *int - if err != nil || parts[5] == "(none)" { - epoch = nil - } else { - epoch = &converted + if parts[5] != "(none)" { + epoch = parseEpoch(parts[8]) } - converted, err = strconv.Atoi(parts[6]) var size int - if err == nil { + if converted, err := strconv.Atoi(parts[6]); err == nil { size = converted } return &pkg.RpmDBEntry{ diff --git a/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go b/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go index fdc80dce5..5c1f2004f 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go @@ -1,8 +1,12 @@ package redhat import ( + "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" @@ -80,6 +84,23 @@ func TestParseRpmManifest(t *testing.T) { Vendor: "Microsoft Corporation", }, }, + { + Name: "vim", + Version: "2:9.0-1.cm2", + PURL: "pkg:rpm/vim@9.0-1.cm2?arch=x86_64&epoch=2&upstream=vim-9.0-1.cm2.src.rpm", + Locations: file.NewLocationSet(location), + Type: pkg.RpmPkg, + Metadata: pkg.RpmDBEntry{ + Name: "vim", + Epoch: intRef(2), + Arch: "x86_64", + Release: "1.cm2", + Version: "9.0", + SourceRpm: "vim-9.0-1.cm2.src.rpm", + Size: 45000, + Vendor: "Microsoft Corporation", + }, + }, } pkgtest.NewCatalogTester(). @@ -88,3 +109,74 @@ func TestParseRpmManifest(t *testing.T) { TestParser(t, parseRpmManifest) } + +func TestNewMetadataFromManifestLine(t *testing.T) { + // fields are: NAME, VERSION-RELEASE, INSTALLTIME, BUILDTIME, VENDOR, EPOCH, SIZE, ARCH, EPOCHNUM, SOURCERPM + line := func(epoch, size, epochNum string) string { + return strings.Join([]string{ + "vim", "9.0-1.cm2", "1653816591", "1653753130", "Microsoft Corporation", + epoch, size, "x86_64", epochNum, "vim-9.0-1.cm2.src.rpm", + }, "\t") + } + + tests := []struct { + name string + entry string + wantEpoch *int + wantSize int + wantErr require.ErrorAssertionFunc + }{ + { + name: "no epoch", + entry: line("(none)", "45000", "0"), + wantEpoch: nil, + wantSize: 45000, + }, + { + name: "explicit zero epoch is distinct from no epoch", + entry: line("0", "45000", "0"), + wantEpoch: intRef(0), + wantSize: 45000, + }, + { + name: "unparsable epochnum yields no epoch", + entry: line("2", "45000", ""), + wantEpoch: nil, + wantSize: 45000, + }, + { + name: "unparsable size yields zero size", + entry: line("2", "bogus", "2"), + wantEpoch: intRef(2), + wantSize: 0, + }, + { + name: "too few fields", + entry: "vim\t9.0-1.cm2", + wantErr: require.Error, + }, + { + name: "version field missing the release", + entry: strings.Replace(line("(none)", "45000", "0"), "9.0-1.cm2", "9.0", 1), + wantErr: require.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + metadata, err := newMetadataFromManifestLine(tt.entry) + tt.wantErr(t, err) + if err != nil { + return + } + + require.NotNil(t, metadata) + assert.Equal(t, tt.wantEpoch, metadata.Epoch) + assert.Equal(t, tt.wantSize, metadata.Size) + }) + } +} diff --git a/syft/pkg/cataloger/redhat/testdata/container-manifest-2 b/syft/pkg/cataloger/redhat/testdata/container-manifest-2 index 1b934cd2c..5a95aedaa 100644 --- a/syft/pkg/cataloger/redhat/testdata/container-manifest-2 +++ b/syft/pkg/cataloger/redhat/testdata/container-manifest-2 @@ -2,4 +2,5 @@ mariner-release 2.0-12.cm2 1653816591 1653753130 Microsoft Corporation (none) 58 filesystem 1.1-9.cm2 1653816591 1653628924 Microsoft Corporation (none) 7596 x86_64 0 filesystem-1.1-9.cm2.src.rpm glibc 2.35-2.cm2 1653816591 1653628955 Microsoft Corporation (none) 10855265 x86_64 0 glibc-2.35-2.cm2.src.rpm openssl-libs 1.1.1k-15.cm2 1653816591 1653631609 Microsoft Corporation (none) 4365048 x86_64 0 openssl-1.1.1k-15.cm2.src.rpm +vim 9.0-1.cm2 1653816591 1653753130 Microsoft Corporation 2 45000 x86_64 2 vim-9.0-1.cm2.src.rpm From 7ca1f22395c788a544288ec98b55527c364be0d9 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu Date: Fri, 21 Aug 2026 11:53:45 -0700 Subject: [PATCH 150/168] fix(dotnet): correct inverted dependency-of relationship direction in packages.lock.json parser (#5143) Fixes #5125 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- .../cataloger/dotnet/parse_packages_lock.go | 4 +-- .../dotnet/parse_packages_lock_test.go | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/syft/pkg/cataloger/dotnet/parse_packages_lock.go b/syft/pkg/cataloger/dotnet/parse_packages_lock.go index 99535d9da..3348d5a0f 100644 --- a/syft/pkg/cataloger/dotnet/parse_packages_lock.go +++ b/syft/pkg/cataloger/dotnet/parse_packages_lock.go @@ -101,8 +101,8 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi } rel := artifact.Relationship{ - From: parentPkg, - To: childPkg, + From: childPkg, + To: parentPkg, Type: artifact.DependencyOfRelationship, } relationships = append(relationships, rel) diff --git a/syft/pkg/cataloger/dotnet/parse_packages_lock_test.go b/syft/pkg/cataloger/dotnet/parse_packages_lock_test.go index 4d8a2c2d7..24ce33454 100644 --- a/syft/pkg/cataloger/dotnet/parse_packages_lock_test.go +++ b/syft/pkg/cataloger/dotnet/parse_packages_lock_test.go @@ -169,28 +169,28 @@ func TestParseDotnetPackagesLock(t *testing.T) { expectedRelationships := []artifact.Relationship{ { - From: autoMapperPkg, + From: extensionOptionsPkg, + To: autoMapperPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: dependencyInjectionAbstractionsPkg, To: extensionOptionsPkg, Type: artifact.DependencyOfRelationship, }, - { - From: extensionOptionsPkg, - To: dependencyInjectionAbstractionsPkg, - Type: artifact.DependencyOfRelationship, - }, - { - From: extensionOptionsPkg, - To: extensionPrimitivesPkg, - Type: artifact.DependencyOfRelationship, - }, { From: extensionPrimitivesPkg, - To: compilerServicesUnsafePkg, + To: extensionOptionsPkg, Type: artifact.DependencyOfRelationship, }, { - From: microsoftLoggingPkg, - To: extensionOptionsPkg, + From: compilerServicesUnsafePkg, + To: extensionPrimitivesPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: extensionOptionsPkg, + To: microsoftLoggingPkg, Type: artifact.DependencyOfRelationship, }, } From bf82010f3c38508e30dd01ed8053f158513c0c9d Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Fri, 21 Aug 2026 22:36:47 +0100 Subject: [PATCH 151/168] fix(lua): skip rockspec with no package name (#4825) --------- Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- syft/pkg/cataloger/lua/parse_rockspec.go | 5 +++ syft/pkg/cataloger/lua/parse_rockspec_test.go | 45 +++++++++++++++++++ syft/pkg/cataloger/lua/rockspec_parser.go | 4 +- .../rockspec/build-first-1.0-1.rockspec | 19 ++++++++ .../lua/testdata/rockspec/empty.rockspec | 0 .../rockspec/whitespace-only.rockspec | 2 + 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 syft/pkg/cataloger/lua/testdata/rockspec/build-first-1.0-1.rockspec create mode 100644 syft/pkg/cataloger/lua/testdata/rockspec/empty.rockspec create mode 100644 syft/pkg/cataloger/lua/testdata/rockspec/whitespace-only.rockspec diff --git a/syft/pkg/cataloger/lua/parse_rockspec.go b/syft/pkg/cataloger/lua/parse_rockspec.go index f49107688..82646aa67 100644 --- a/syft/pkg/cataloger/lua/parse_rockspec.go +++ b/syft/pkg/cataloger/lua/parse_rockspec.go @@ -74,6 +74,11 @@ func parseRockspec(ctx context.Context, resolver file.Resolver, _ *generic.Envir } } + if name == "" { + log.WithFields("path", reader.Path()).Trace("rockspec has no package name, skipping") + return nil, nil, nil + } + p := newLuaRocksPackage( ctx, resolver, diff --git a/syft/pkg/cataloger/lua/parse_rockspec_test.go b/syft/pkg/cataloger/lua/parse_rockspec_test.go index f97ecc866..7fdc33c04 100644 --- a/syft/pkg/cataloger/lua/parse_rockspec_test.go +++ b/syft/pkg/cataloger/lua/parse_rockspec_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -144,6 +145,31 @@ func TestParseRockspec(t *testing.T) { }, }, }, + { + // a build block before the package/version fields must not truncate the parse + Fixture: "testdata/rockspec/build-first-1.0-1.rockspec", + ExpectedPkg: pkg.Package{ + Name: "foo", + Version: "1.0-1", + PURL: "pkg:luarocks/foo@1.0-1", + Type: pkg.LuaRocksPkg, + Language: pkg.Lua, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("testdata/rockspec/build-first-1.0-1.rockspec")), + ), + Metadata: pkg.LuaRocksPackage{ + Name: "foo", + Version: "1.0-1", + License: "MIT", + Homepage: "https://github.com/example/foo", + Description: "an example rock", + URL: "git+https://github.com/example/foo.git", + Dependencies: map[string]string{ + "lua": ">= 5.1", + }, + }, + }, + }, } for _, test := range tests { @@ -154,6 +180,25 @@ func TestParseRockspec(t *testing.T) { } } +func TestParseRockspec_noPackageName(t *testing.T) { + // an empty or whitespace-only rockspec has no package name, so it should + // yield no packages and no error rather than a nameless package + fixtures := []string{ + "testdata/rockspec/empty.rockspec", + "testdata/rockspec/whitespace-only.rockspec", + } + + for _, fixture := range fixtures { + t.Run(fixture, func(t *testing.T) { + pkgtest.NewCatalogTester(). + FromFile(t, fixture). + WithErrorAssertion(require.NoError). + Expects(nil, nil). + TestParser(t, parseRockspec) + }) + } +} + func Test_corruptRockspec(t *testing.T) { pkgtest.NewCatalogTester(). FromFile(t, "testdata/corrupt/bad-1.23.0-0.rockspec"). diff --git a/syft/pkg/cataloger/lua/rockspec_parser.go b/syft/pkg/cataloger/lua/rockspec_parser.go index be8630293..14317b5f0 100644 --- a/syft/pkg/cataloger/lua/rockspec_parser.go +++ b/syft/pkg/cataloger/lua/rockspec_parser.go @@ -548,11 +548,11 @@ func skipBuildNode(data []byte, i *int) { bracesCount-- } + *i++ + if bracesCount == 0 { return } - - *i++ } } diff --git a/syft/pkg/cataloger/lua/testdata/rockspec/build-first-1.0-1.rockspec b/syft/pkg/cataloger/lua/testdata/rockspec/build-first-1.0-1.rockspec new file mode 100644 index 000000000..94b3f4490 --- /dev/null +++ b/syft/pkg/cataloger/lua/testdata/rockspec/build-first-1.0-1.rockspec @@ -0,0 +1,19 @@ +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +package = "foo" +version = "1.0-1" +source = { + url = "git+https://github.com/example/foo.git" +} +description = { + summary = "an example rock", + homepage = "https://github.com/example/foo", + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} diff --git a/syft/pkg/cataloger/lua/testdata/rockspec/empty.rockspec b/syft/pkg/cataloger/lua/testdata/rockspec/empty.rockspec new file mode 100644 index 000000000..e69de29bb diff --git a/syft/pkg/cataloger/lua/testdata/rockspec/whitespace-only.rockspec b/syft/pkg/cataloger/lua/testdata/rockspec/whitespace-only.rockspec new file mode 100644 index 000000000..925bd69ca --- /dev/null +++ b/syft/pkg/cataloger/lua/testdata/rockspec/whitespace-only.rockspec @@ -0,0 +1,2 @@ + + From d3734ddeb2d6a7e861355dd6facb730dd4d46537 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu Date: Wed, 26 Aug 2026 21:20:09 -0700 Subject: [PATCH 152/168] fix(binary): detect grafana security-patch release versions (#5213) --------- Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- .../binary/classifier_cataloger_test.go | 33 ++++++++++++++++++ syft/pkg/cataloger/binary/classifiers.go | 5 +++ .../11.0.5-security-01/linux-amd64/grafana | Bin 0 -> 352 bytes .../12.0.0-security-01/linux-amd64/grafana | Bin 0 -> 352 bytes .../12.4.3-security-02/linux-arm/grafana | Bin 0 -> 353 bytes .../pkg/cataloger/binary/testdata/config.yaml | 24 +++++++++++++ 6 files changed, 62 insertions(+) create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/11.0.5-security-01/linux-amd64/grafana create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.0.0-security-01/linux-amd64/grafana create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.4.3-security-02/linux-arm/grafana diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index d8d3567ff..4b4e2ed7d 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -2294,6 +2294,39 @@ func Test_Cataloger_PositiveCases(t *testing.T) { Metadata: metadata("grafana-binary"), }, }, + { + logicalFixture: "grafana/12.4.3-security-02/linux-arm", + expected: pkg.Package{ + Name: "grafana", + Version: "12.4.3", + Type: "binary", + PURL: "pkg:generic/grafana@12.4.3", + Locations: locations("grafana"), + Metadata: metadata("grafana-binary"), + }, + }, + { + logicalFixture: "grafana/12.0.0-security-01/linux-amd64", + expected: pkg.Package{ + Name: "grafana", + Version: "12.0.0", + Type: "binary", + PURL: "pkg:generic/grafana@12.0.0", + Locations: locations("grafana"), + Metadata: metadata("grafana-binary"), + }, + }, + { + logicalFixture: "grafana/11.0.5-security-01/linux-amd64", + expected: pkg.Package{ + Name: "grafana", + Version: "11.0.5", + Type: "binary", + PURL: "pkg:generic/grafana@11.0.5", + Locations: locations("grafana"), + Metadata: metadata("grafana-binary"), + }, + }, { logicalFixture: "grafana/12.3.1/linux-amd64", expected: pkg.Package{ diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 26fd9ce7f..5fc3c7b80 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -1054,6 +1054,11 @@ func DefaultClassifiers() []binutils.Classifier { EvidenceMatcher: binutils.MatchAny( // [NUL][NUL][NUL][NUL]12.2.0-258092[NUL][NUL][NUL][NUL] m.FileContentsVersionMatcher(`\x00+(?P[0-9]{2}\.[0-9]+\.[0-9]+\-[0-9]{6,})\x00+`), + // security patch releases embed the raw version constant with no "release-" prefix + // and are not always preceded by NUL bytes (e.g. on arm builds): + // [NUL]11.0.5+security-01[NUL][NUL]call frame too large + // [NUL]12.4.3+security-02[NUL][NUL] + m.FileContentsVersionMatcher(`\x00(?P[0-9]{1,2}\.[0-9]+\.[0-9]+)\+security-[0-9]+\x00`), // [NUL][NUL][NUL][NUL]release-12.3.2+security-01[NUL][NUL][NUL][NUL] // [NUL][NUL][NUL][NUL]release-12.3.1[NUL][NUL][NUL][NUL] m.FileContentsVersionMatcher(`\x00+release-(?P[0-9]{2}\.[0-9]+\.[0-9]+(-beta[0-9]|-test|-preview)?)(\+security-[0-9]+)?\x00+`), diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/11.0.5-security-01/linux-amd64/grafana b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/11.0.5-security-01/linux-amd64/grafana new file mode 100644 index 0000000000000000000000000000000000000000..0aacb4de3d5815d4b7673d4aa41d3b9453d789cb GIT binary patch literal 352 zcmXv~yKciU5NlU`KxY9Niv~J9BY#Afe!=NaX>aI(AEKB<4T@5u0#Pag-gA$FRwwUK-VNH$sIC_qTg zR7wWDlq^yaiDC$_OF2~7Igk>;dCD$=l4IkwOtMAi8JQfUB;>2{JlBddMXF0Bmujn! zVDN>ZKuo}iYKegghpg0Qv(e=`v3|_d)1YN&ZTQl2q@HHDpZDiQUly7Hur=H8dSUt; okJI(dz};V&ur)2?{GqvQ&<)2uui2uvJHGGU??eA?>&+7X15e9f%m4rY literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.0.0-security-01/linux-amd64/grafana b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.0.0-security-01/linux-amd64/grafana new file mode 100644 index 0000000000000000000000000000000000000000..c8b3fbe79e08c2f3d9467e5f0726b86367bed4be GIT binary patch literal 352 zcmX|7%TB{U46MX$zhb2v5!KrJ%H}ut0;jRlN=s5DL8vFh5Ab_j`2;RV+{%WQ&cQR9 zu|}2$dg4a+6A2BZMv$ z*kG-vqH2oC3JHU7O<;|PX_-*4tjE-POr4TL=37}pgf^MbhRD$Xr49nbx^+x8y68La zqfdY-#?l#GvCvUO4#}FDm8z;rx2J{m^-R5tTE?8m7hNK?S@HdLx6%Vm%7;qXs54DH-@+o!7rb7)uPgtY|995-7dtLsIsgCw literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.4.3-security-02/linux-arm/grafana b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.4.3-security-02/linux-arm/grafana new file mode 100644 index 0000000000000000000000000000000000000000..44469b80a156b8f42c9e5092b4c59eb011759d29 GIT binary patch literal 353 zcmY+8y-EW?9K;u^%XSu~UQ8*5{kZ)K7!U+aZEbP8_upLLybH;RNh=nDuiy*V1j|&x z(#j&1BH9SvTX+FeI55Th28Jn;OX!i|Bne58;4*|MR6W8B*HlW)aSp|>${NBco))9= z7^?Fu;ZpTTnrdcDOgW=IFbXDS)LLNNcmY}nMH#c!)6{!y1jI@h9bzfLXlZGPaTw*W z9dn?;C>J~Kz-s4R%9ZfKX``s%Dp;3r1CpmyFzKa9v{Y#jJTx>|V=(S?I>gN@Ab&SW zRgzHVd3jBm3Zwtp-$cf{aaSBn!Cy^A)%=ihgrbu=diubv$H8Lpa@0oIaT_iBbu>Gz pqldxU{q5Z@`f8k=)zR`3w^seV_49fMZM Date: Thu, 27 Aug 2026 09:30:58 -0400 Subject: [PATCH 153/168] chore(deps): update CPE dictionary index (#5221) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 8e09644d1..304cc59a2 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -178,6 +178,9 @@ "github.com/pocketbase/pocketbase": [ "cpe:2.3:a:pocketbase:pocketbase:*:*:*:*:*:go:*:*" ], + "github.com/prebid/prebid-server": [ + "cpe:2.3:a:prebid:prebid_server:*:*:*:*:*:go:*:*" + ], "github.com/proglottis/gpgme": [ "cpe:2.3:a:gpgme_project:gpgme:*:*:*:*:*:go:*:*" ], @@ -3676,6 +3679,9 @@ "feathers-sequelize": [ "cpe:2.3:a:feathersjs:feathers-sequelize:*:*:*:*:*:node.js:*:*" ], + "fflate": [ + "cpe:2.3:a:101arrowz:fflate:*:*:*:*:*:node.js:*:*" + ], "ffmepg": [ "cpe:2.3:a:ffmepg_project:ffmepg:*:*:*:*:*:node.js:*:*" ], @@ -3943,6 +3949,7 @@ "cpe:2.3:a:hapi-auth-jwt2_project:hapi-auth-jwt2:*:*:*:*:*:node.js:*:*" ], "happy-dom": [ + "cpe:2.3:a:capricorn86:happy_dom:*:*:*:*:*:node.js:*:*", "cpe:2.3:a:capricorn86:happy_dom:*:*:*:*:*:nodejs:*:*" ], "harp": [ @@ -6527,6 +6534,9 @@ "b2sdk": [ "cpe:2.3:a:backblaze:b2-sdk-python:*:*:*:*:*:*:*:*" ], + "bbot": [ + "cpe:2.3:a:blacklanternsecurity:bbot:*:*:*:*:*:*:*:*" + ], "bedrock-agentcore-starter-toolkit": [ "cpe:2.3:a:amazon:bedrock_agentcore_starter_toolkit:*:*:*:*:*:python:*:*" ], @@ -6618,6 +6628,9 @@ "datapizza-ai": [ "cpe:2.3:a:datapizza:datapizza_ai:*:*:*:*:*:*:*:*" ], + "datasets": [ + "cpe:2.3:a:huggingface:datasets:*:*:*:*:*:python:*:*" + ], "dbt-common": [ "cpe:2.3:a:getdbt:dbt-common:*:*:*:*:*:*:*:*" ], @@ -7260,6 +7273,9 @@ "cremefraiche": [ "cpe:2.3:a:uplawski:creme_fraiche:*:*:*:*:*:ruby:*:*" ], + "css_parser": [ + "cpe:2.3:a:premailer:css_parser:*:*:*:*:*:ruby:*:*" + ], "csv-safe": [ "cpe:2.3:a:csv-safe_project:csv-safe:*:*:*:*:*:ruby:*:*" ], @@ -8039,12 +8055,18 @@ "magnetic": [ "cpe:2.3:a:magnetic_project:magnetic:*:*:*:*:*:rust:*:*" ], + "markdown-tui-explorer": [ + "cpe:2.3:a:leboiko:markdown-tui-explorer:*:*:*:*:*:rust:*:*" + ], "may": [ "cpe:2.3:a:may_project:may:*:*:*:*:*:rust:*:*" ], "mdbook": [ "cpe:2.3:a:rust-lang:mdbook:*:*:*:*:*:rust:*:*" ], + "mermaid-text": [ + "cpe:2.3:a:leboiko:mermaid-text:*:*:*:*:*:rust:*:*" + ], "messagepack-rs": [ "cpe:2.3:a:messagepack-rs_project:messagepack-rs:*:*:*:*:*:rust:*:*" ], @@ -8093,6 +8115,9 @@ "one2html": [ "cpe:2.3:a:msiemens:one2html:*:*:*:*:*:rust:*:*" ], + "onenote_parser": [ + "cpe:2.3:a:msiemens:rust_onenote_file_parser:*:*:*:*:*:rust:*:*" + ], "openssl": [ "cpe:2.3:a:sfackler:openssl:*:*:*:*:*:rust:*:*" ], @@ -8135,11 +8160,14 @@ "quinn": [ "cpe:2.3:a:quinn_project:quinn:*:*:*:*:*:rust:*:*" ], + "quote": [ + "cpe:2.3:a:dtolnay:quote:*:*:*:*:*:rust:*:*" + ], "qwutils": [ "cpe:2.3:a:qwutils_project:qwutils:*:*:*:*:*:rust:*:*" ], "rand": [ - "cpe:2.3:a:rand_project:rand:*:*:*:*:*:*:*:*" + "cpe:2.3:a:rust-random:rand:*:*:*:*:*:rust:*:*" ], "rand_core": [ "cpe:2.3:a:rand_core_project:rand_core:*:*:*:*:*:rust:*:*" @@ -8231,6 +8259,9 @@ "telemetry": [ "cpe:2.3:a:telemetry_project:telemetry:*:*:*:*:*:rust:*:*" ], + "termimad": [ + "cpe:2.3:a:canop:termimad:*:*:*:*:*:rust:*:*" + ], "thex": [ "cpe:2.3:a:thex_project:thex:*:*:*:*:*:rust:*:*" ], @@ -10641,6 +10672,9 @@ "clickdesk-live-support-chat-plugin": [ "cpe:2.3:a:clickdesk:clickdesk:*:*:*:*:*:wordpress:*:*" ], + "clicksold-wordpress-plugin": [ + "cpe:2.3:a:clicksold:clicksold_idx:*:*:*:*:*:wordpress:*:*" + ], "clickwhale": [ "cpe:2.3:a:flowdee:clickwhale:*:*:*:*:*:wordpress:*:*" ], @@ -12726,6 +12760,9 @@ "fancybox-for-wordpress": [ "cpe:2.3:a:colorlib:fancybox:*:*:*:*:*:wordpress:*:*" ], + "fantastic-elasticsearch": [ + "cpe:2.3:a:parisholley:fantastic_elasticsearch:*:*:*:*:*:wordpress:*:*" + ], "faq-builder-ays": [ "cpe:2.3:a:ays-pro:faq_builder:*:*:*:*:*:wordpress:*:*" ], @@ -13426,11 +13463,15 @@ "cpe:2.3:a:brightplugins:block_ips_for_gravity_forms:*:*:*:*:*:wordpress:*:*" ], "gf-constant-contact": [ + "cpe:2.3:a:crmperks:wp_gravity_forms_constant_contact_plugin:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:rocketgenius:constant_contact:*:*:*:*:*:wordpress:*:*" ], "gf-custom-style": [ "cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*" ], + "gf-freshdesk": [ + "cpe:2.3:a:crmperks:wp_gravity_forms_freshdesk_plugin:*:*:*:*:*:wordpress:*:*" + ], "gf-hubspot": [ "cpe:2.3:a:crmperks:wp_gravity_forms_hubspot:*:*:*:*:*:wordpress:*:*" ], @@ -13642,6 +13683,9 @@ "gourl-bitcoin-payment-gateway-paid-downloads-membership": [ "cpe:2.3:a:gorul:gourl:*:*:*:*:*:wordpress:*:*" ], + "gozen-forms": [ + "cpe:2.3:a:optinlyhq:gozen_forms:*:*:*:*:*:wordpress:*:*" + ], "gpt3-ai-content-generator": [ "cpe:2.3:a:aipower:aipower:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:gptaipower:gpt_ai_power:*:*:*:*:*:wordpress:*:*" @@ -15866,6 +15910,9 @@ "music-player-for-elementor": [ "cpe:2.3:a:smartwpress:music_player_for_elementor:*:*:*:*:*:wordpress:*:*" ], + "music-sheet-viewer": [ + "cpe:2.3:a:partitionnumerique:music_sheet_viewer:*:*:*:*:*:wordpress:*:*" + ], "music-store": [ "cpe:2.3:a:codepeople:music_store:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:musicstore:music_store:*:*:*:*:*:wordpress:*:*" @@ -16266,9 +16313,15 @@ "olive-one-click-demo-import": [ "cpe:2.3:a:olivethemes:olive_one_click_demo_import:*:*:*:*:*:wordpress:*:*" ], + "oliver-pos": [ + "cpe:2.3:a:oliverpos:oliver_pos:*:*:*:*:*:wordpress:*:*" + ], "olympus-google-fonts": [ "cpe:2.3:a:fontsplugin:fonts:*:*:*:*:*:wordpress:*:*" ], + "omnipress": [ + "cpe:2.3:a:omnipressteam:omnipress:*:*:*:*:*:wordpress:*:*" + ], "omnisend-connect": [ "cpe:2.3:a:omnisend:email_marketing_for_woocommerce:*:*:*:*:*:wordpress:*:*" ], @@ -17320,6 +17373,9 @@ "push-notifications-for-wp": [ "cpe:2.3:a:delitestudio:push_notifications_for_wordpress:*:*:*:*:lite:wordpress:*:*" ], + "pushbiz": [ + "cpe:2.3:a:csimplifyit:pushbiz:*:*:*:*:*:wordpress:*:*" + ], "pwa-for-wp": [ "cpe:2.3:a:magazine3:pwa_for_wp_\\\u0026_amp:*:*:*:*:*:wordpress:*:*" ], @@ -17404,6 +17460,9 @@ "quick-subscribe": [ "cpe:2.3:a:quick_subscribe_project:quick_subscribe:*:*:*:*:*:wordpress:*:*" ], + "quicksand-jquery-post-filter": [ + "cpe:2.3:a:developingtheweb:quicksand_post_filter_jquery:*:*:*:*:*:wordpress:*:*" + ], "quickswish": [ "cpe:2.3:a:hasthemes:quickswish:*:*:*:*:*:wordpress:*:*" ], @@ -17849,6 +17908,9 @@ "robo-gallery": [ "cpe:2.3:a:robogallery:robo_gallery:*:*:*:*:*:wordpress:*:*" ], + "robo-maps": [ + "cpe:2.3:a:robosoft:maps:*:*:*:*:*:wordpress:*:*" + ], "rock-convert": [ "cpe:2.3:a:rockcontent:rock_convert:*:*:*:*:*:wordpress:*:*" ], @@ -19235,6 +19297,9 @@ "sticky-popup": [ "cpe:2.3:a:sticky_popup_project:sticky_popup:*:*:*:*:*:wordpress:*:*" ], + "stklcode-liveticker": [ + "cpe:2.3:a:stklcode:liveticker:*:*:*:*:*:wordpress:*:*" + ], "stock-in": [ "cpe:2.3:a:stock_in_\\\u0026_out_project:stock_in_\\\u0026_out:*:*:*:*:*:wordpress:*:*" ], @@ -20097,6 +20162,9 @@ "ultimate-reviews": [ "cpe:2.3:a:etoilewebdesign:ultimate_reviews:*:*:*:*:*:wordpress:*:*" ], + "ultimate-shortcodes-creator": [ + "cpe:2.3:a:cmorillas1:shortcodes_blocks_creator_ultimate:*:*:*:*:*:wordpress:*:*" + ], "ultimate-sms-notifications": [ "cpe:2.3:a:homescript:ultimate_sms_notifications:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:ultimatesmsnotifications:ultimate_sms_notifications_for_woocommerce:*:*:*:*:*:wordpress:*:*" @@ -20969,6 +21037,7 @@ "cpe:2.3:a:majas-lapu-izstrade:cartbounty:*:*:*:*:*:wordpress:*:*" ], "woo-shipping-dpd-baltic": [ + "cpe:2.3:a:dpd:dpd_baltic_shipping:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:dpdgroup:woocommerce_shipping:*:*:*:*:*:wordpress:*:*" ], "woo-slider-pro-drag-drop-slider-builder-for-woocommerce": [ @@ -22242,6 +22311,9 @@ "wp-prayers-request": [ "cpe:2.3:a:goprayer:prayer:*:*:*:*:*:wordpress:*:*" ], + "wp-pricing-table": [ + "cpe:2.3:a:codecabin:wp_pricing_table:*:*:*:*:*:wordpress:*:*" + ], "wp-print-friendly": [ "cpe:2.3:a:oomphinc:wp_print_friendly:*:*:*:*:*:wordpress:*:*" ], @@ -22959,6 +23031,9 @@ "wpstream": [ "cpe:2.3:a:wpstream:wpstream:*:*:*:*:*:wordpress:*:*" ], + "wpsyncsheets-wpforms": [ + "cpe:2.3:a:creativewerkdesigns:wpsyncsheets:*:*:*:*:lite:wordpress:*:*" + ], "wptemplata": [ "cpe:2.3:a:hasthemes:wp_templata:*:*:*:*:*:wordpress:*:*" ], @@ -23711,6 +23786,9 @@ "sparkling": [ "cpe:2.3:a:colorlib:sparklinkg:*:*:*:*:*:wordpress:*:*" ], + "storely": [ + "cpe:2.3:a:sellerthemes:storely:*:*:*:*:free:wordpress:*:*" + ], "teluro": [ "cpe:2.3:a:extendthemes:teluro:*:*:*:*:*:wordpress:*:*" ], From f91bf4528b48385761ea1213895d382890bfd1ee Mon Sep 17 00:00:00 2001 From: Ankush Pathak Date: Thu, 27 Aug 2026 20:25:31 +0530 Subject: [PATCH 154/168] fix: correct Apache Derby group ID in purl generation (#5090) Signed-off-by: Ankush Pathak --- .../internal/cpegenerate/java_groupid_map.go | 6 ++++++ syft/pkg/cataloger/java/package_url_test.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go index 0e4aa8abd..9dcb71567 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go +++ b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go @@ -274,6 +274,12 @@ var DefaultArtifactIDToGroupID = map[string]string{ "bcutil-jdk15to18": "org.bouncycastle", "bcutil-jdk18on": "org.bouncycastle", "bcutil-lts8on": "org.bouncycastle", + "derby": "org.apache.derby", + "derbyclient": "org.apache.derby", + "derbynet": "org.apache.derby", + "derbyoptionaltools": "org.apache.derby", + "derbyshared": "org.apache.derby", + "derbytools": "org.apache.derby", "elasticsearch": "org.elasticsearch", "elasticsearch-analysis-icu": "org.elasticsearch", "elasticsearch-analysis-ik": "org.elasticsearch", diff --git a/syft/pkg/cataloger/java/package_url_test.go b/syft/pkg/cataloger/java/package_url_test.go index 5433b1481..5bb92fd00 100644 --- a/syft/pkg/cataloger/java/package_url_test.go +++ b/syft/pkg/cataloger/java/package_url_test.go @@ -202,6 +202,22 @@ func Test_groupIDFromJavaMetadata(t *testing.T) { metadata: pkg.JavaArchive{}, expect: "org.codehaus.jackson", }, + { + // regression: Apache Derby jars are OSGi bundles that ship no pom + // metadata, so the manifest heuristic would otherwise pick up + // Bundle-Activator (org.apache.derby.osgi.EmbeddedActivator) as the + // group ID. The known package list must win instead. + name: "known package list derby", + pkgName: "derby", + metadata: pkg.JavaArchive{}, + expect: "org.apache.derby", + }, + { + name: "known package list derbytools", + pkgName: "derbytools", + metadata: pkg.JavaArchive{}, + expect: "org.apache.derby", + }, { name: "java manifest", metadata: pkg.JavaArchive{ From 91a0032987d91b7411b52f6f5c185c5e7f775495 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Thu, 27 Aug 2026 12:25:09 -0400 Subject: [PATCH 155/168] chore(deps): update anchore dependencies (#5085) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 36dc71f64..6b8502ec1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/anchore/go-sync v0.1.1 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.2.0 - github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf + github.com/anchore/stereoscope v0.3.1 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef @@ -82,7 +82,7 @@ require ( github.com/spdx/tools-golang v0.6.0-rc4 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.12.0 + github.com/stretchr/testify v1.12.1 github.com/ulikunitz/xz v0.5.16 github.com/vbatts/go-mtree v0.7.0 github.com/vifraa/gopom v1.0.0 @@ -170,13 +170,13 @@ require ( github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect github.com/containerd/containerd/api v1.11.1 // indirect - github.com/containerd/containerd/v2 v2.3.3 // indirect + github.com/containerd/containerd/v2 v2.3.4 // indirect github.com/containerd/continuity v0.5.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v1.0.0-rc.4 // indirect + github.com/containerd/platforms v1.0.0-rc.5 // indirect github.com/containerd/plugin v1.1.0 // indirect github.com/containerd/ttrpc v1.2.8 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect diff --git a/go.sum b/go.sum index 7e280ae90..79321e023 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= -github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf h1:eTQTL06tadOz+V4ubhXOlc5oKQ11cXbyo/clnxlcsnU= -github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf/go.mod h1:cWQ8fLFblnkJ0c3BSFY+BwXe4DcJDdysP/gnoWHVb7s= +github.com/anchore/stereoscope v0.3.1 h1:SFLeRyi+3L0fVDTcd4sjT6Xalvi9pju7DegeUMmxna8= +github.com/anchore/stereoscope v0.3.1/go.mod h1:KHLeUz03/lpsxM8jvs7gFSWRHCXB9MLnxpusCIPe3ME= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -294,8 +294,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k= -github.com/containerd/containerd/v2 v2.3.3/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= +github.com/containerd/containerd/v2 v2.3.4 h1:c2PJo/9UGVdiiw8SwrxuLxWGY+9b3jQ6Xp9zntneIvI= +github.com/containerd/containerd/v2 v2.3.4/go.mod h1:a30D8fWZJ1Uzx/2WpjLbLsxBkq9He41pe8ENW+QZ3LY= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -306,8 +306,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= -github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= +github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= +github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= @@ -918,8 +918,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= -github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= From 1417c1989b193025d2e4132ec79b1ee6e9019176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:31:08 +0000 Subject: [PATCH 156/168] chore(deps): bump modernc.org/sqlite from 1.56.0 to 1.57.0 (#5238) Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.56.0 to 1.57.0. - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.56.0...v1.57.0) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.57.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6b8502ec1..1f01748e4 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.49.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.56.0 + modernc.org/sqlite v1.57.0 ) require ( diff --git a/go.sum b/go.sum index 79321e023..aa0ea3b85 100644 --- a/go.sum +++ b/go.sum @@ -1536,8 +1536,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= -modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= +modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg= +modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 82d273fac52cf2fce01aecf73598bbba13162121 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:32:35 +0000 Subject: [PATCH 157/168] chore(deps): bump github.com/gpustack/gguf-parser-go (#5239) Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.25.0 to 0.26.0. - [Release notes](https://github.com/gpustack/gguf-parser-go/releases) - [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.25.0...v0.26.0) --- updated-dependencies: - dependency-name: github.com/gpustack/gguf-parser-go dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f01748e4..c69bdab18 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/google/licensecheck v0.3.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.1 - github.com/gpustack/gguf-parser-go v0.25.0 + github.com/gpustack/gguf-parser-go v0.26.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-getter v1.8.8 github.com/hashicorp/go-multierror v1.1.1 diff --git a/go.sum b/go.sum index aa0ea3b85..917b6d66c 100644 --- a/go.sum +++ b/go.sum @@ -556,8 +556,8 @@ github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU= github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= -github.com/gpustack/gguf-parser-go v0.25.0 h1:1AMBhMKtI24nTtn588Bq53FqNiOvEw1x9Nb4HbRrThs= -github.com/gpustack/gguf-parser-go v0.25.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= +github.com/gpustack/gguf-parser-go v0.26.0 h1:klBqNj3w5oyY2GWC6/aQJ6MtPUYtHyYRlEyzEf00cxA= +github.com/gpustack/gguf-parser-go v0.26.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA= From 40179796705568e3adb8e823972e3a1cea1b18fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:33:27 +0000 Subject: [PATCH 158/168] chore(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#5237) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8218feecf..15676169f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -86,7 +86,7 @@ jobs: # dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a # docker-container driver builder (the default builder on the runner cannot build multi-platform). - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Login to Docker Hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0 From af937d74c00c26374449e81aa4ff45ae2d668a33 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Sat, 29 Aug 2026 06:24:02 -0400 Subject: [PATCH 159/168] chore(deps): update tool versions (#5242) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .binny.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.binny.yaml b/.binny.yaml index da8e9cab2..197d52ede 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -24,7 +24,7 @@ tools: # used to parse JSON/YAML annotations on the fixture cache image - name: yq version: - want: v4.53.3 + want: v4.53.6 method: github-release with: repo: mikefarah/yq From cc13d1dbf1d0918a1bbd873d21be338249089f66 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Mon, 31 Aug 2026 13:03:10 -0400 Subject: [PATCH 160/168] chore(deps): update CPE dictionary index (#5244) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 123 +++++++++++++++++- 1 file changed, 119 insertions(+), 4 deletions(-) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 304cc59a2..1d8d04a96 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -290,6 +290,12 @@ ], "gopkg.in/yaml.v3": [ "cpe:2.3:a:yaml_project:yaml:*:*:*:*:*:go:*:*" + ], + "oras.land/oras-go": [ + "cpe:2.3:a:linuxfoundation:oras:*:*:*:*:*:go:*:*" + ], + "oras.land/oras-go/v2": [ + "cpe:2.3:a:linuxfoundation:oras:*:*:*:*:*:go:*:*" ] }, "jenkins_plugins": { @@ -2379,6 +2385,9 @@ "@appium/base-driver": [ "cpe:2.3:a:appium:appium\\/base-driver:*:*:*:*:*:node.js:*:*" ], + "@appium/storage-plugin": [ + "cpe:2.3:a:appium:appium\\/storage-plugin:*:*:*:*:*:node.js:*:*" + ], "@appium/support": [ "cpe:2.3:a:appium:appium\\/support:*:*:*:*:*:node.js:*:*" ], @@ -2415,6 +2424,12 @@ "@backstage/plugin-auth-backend": [ "cpe:2.3:a:linuxfoundation:auth_backend:*:*:*:*:*:node.js:*:*" ], + "@backstage/plugin-catalog-backend": [ + "cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-backend-module-unprocessed:*:*:*:*:*:node.js:*:*" + ], + "@backstage/plugin-catalog-unprocessed-entities-common": [ + "cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-unprocessed-entities-common:*:*:*:*:*:node.js:*:*" + ], "@backstage/plugin-scaffolder-backend": [ "cpe:2.3:a:linuxfoundation:backstage\\/plugin-scaffolder-backend:*:*:*:*:*:*:*:*" ], @@ -2599,6 +2614,9 @@ "@lionello/secp256k1-js": [ "cpe:2.3:a:secp256k1-js_project:secp256k1-js:*:*:*:*:*:node.js:*:*" ], + "@marko/runtime-tags": [ + "cpe:2.3:a:openjsf:marko\\/runtime-tags:*:*:*:*:*:node.js:*:*" + ], "@mattkrick/sanitize-svg": [ "cpe:2.3:a:sanitize-svg_project:sanitize-svg:*:*:*:*:*:node.js:*:*" ], @@ -3322,6 +3340,9 @@ "date-and-time": [ "cpe:2.3:a:date-and-time_project:date-and-time:*:*:*:*:*:node.js:*:*" ], + "date-fns": [ + "cpe:2.3:a:date-fns:date-fns:*:*:*:*:*:node.js:*:*" + ], "dawnsparks-node-tesseract": [ "cpe:2.3:a:dawnsparks-node-tesseract_project:dawnsparks-node-tesseract:*:*:*:*:*:node.js:*:*" ], @@ -3949,8 +3970,7 @@ "cpe:2.3:a:hapi-auth-jwt2_project:hapi-auth-jwt2:*:*:*:*:*:node.js:*:*" ], "happy-dom": [ - "cpe:2.3:a:capricorn86:happy_dom:*:*:*:*:*:node.js:*:*", - "cpe:2.3:a:capricorn86:happy_dom:*:*:*:*:*:nodejs:*:*" + "cpe:2.3:a:capricorn86:happy_dom:*:*:*:*:*:node.js:*:*" ], "harp": [ "cpe:2.3:a:npmjs:harp:*:*:*:*:*:*:*:*" @@ -4260,6 +4280,9 @@ "jquey": [ "cpe:2.3:a:jquey_project:jquey:*:*:*:*:*:node.js:*:*" ], + "js-cookie": [ + "cpe:2.3:a:js-cookie:javascript_cookie:*:*:*:*:*:node.js:*:*" + ], "js-given": [ "cpe:2.3:a:js-given_project:js-given:*:*:*:*:*:node.js:*:*" ], @@ -6057,6 +6080,9 @@ "turbo-stream": [ "cpe:2.3:a:turbo-stream:turbo_stream:*:*:*:*:*:node.js:*:*" ], + "type-fest": [ + "cpe:2.3:a:sindresorhus:type-fest:*:*:*:*:*:node.js:*:*" + ], "ua-parser": [ "cpe:2.3:a:ua-parser_project:ua-parser:*:*:*:*:*:node.js:*:*" ], @@ -6842,6 +6868,9 @@ "networkx": [ "cpe:2.3:a:python:networkx:*:*:*:*:*:*:*:*" ], + "neural-compressor": [ + "cpe:2.3:a:intel:neural_compressor:*:*:*:*:*:python:*:*" + ], "nltk": [ "cpe:2.3:a:nltk:nltk:*:*:*:*:*:*:*:*" ], @@ -6869,6 +6898,9 @@ "openssh-key-parser": [ "cpe:2.3:a:openssh_key_parser_project:openssh_key_parser:*:*:*:*:*:python:*:*" ], + "openssl-encrypt": [ + "cpe:2.3:a:jahlives:openssl_encrypt:*:*:*:*:*:python:*:*" + ], "ormar": [ "cpe:2.3:a:collerek:ormar:*:*:*:*:*:python:*:*" ], @@ -8265,6 +8297,9 @@ "thex": [ "cpe:2.3:a:thex_project:thex:*:*:*:*:*:rust:*:*" ], + "thin-vec": [ + "cpe:2.3:a:mozilla:thin-vec:*:*:*:*:*:rust:*:*" + ], "through": [ "cpe:2.3:a:through_project:through:*:*:*:*:*:rust:*:*" ], @@ -8817,6 +8852,9 @@ "cpe:2.3:a:bestwebsoft:google_adsense:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:google_adsense_project:google_adsense:*:*:*:*:*:wordpress:*:*" ], + "advance-post-prefix": [ + "cpe:2.3:a:niceit:advance_post_prefix:*:*:*:*:*:wordpress:*:*" + ], "advance-search": [ "cpe:2.3:a:advance_search_project:advance_search:*:*:*:*:*:wordpress:*:*" ], @@ -9220,6 +9258,9 @@ "animategl": [ "cpe:2.3:a:creativeinteractivemedia:animategl_animations:*:*:*:*:*:wordpress:*:*" ], + "animation-addons-for-elementor-pro": [ + "cpe:2.3:a:crowdytheme:arolax:*:*:*:*:*:wordpress:*:*" + ], "announcer": [ "cpe:2.3:a:aakashweb:announcer:*:*:*:*:*:wordpress:*:*" ], @@ -9544,6 +9585,9 @@ "aviary-image-editor-add-on-for-gravity-forms": [ "cpe:2.3:a:aviary_image_editor_add-on_for_gravity_forms_project:aviary_image_editor_add-on_for_gravity_forms:*:*:*:*:*:wordpress:*:*" ], + "avif-support": [ + "cpe:2.3:a:grandplugins:avif_uploader:*:*:*:*:*:wordpress:*:*" + ], "aweber-web-form-widget": [ "cpe:2.3:a:aweber:aweber:*:*:*:*:*:wordpress:*:*" ], @@ -9630,6 +9674,9 @@ "backwpup": [ "cpe:2.3:a:inpsyde:backwpup:*:*:*:*:*:wordpress:*:*" ], + "badgearoo": [ + "cpe:2.3:a:danielpowney:badgearoo:*:*:*:*:*:wordpress:*:*" + ], "badgeos": [ "cpe:2.3:a:badgeos:badgeos:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:badgeos:badgos:*:*:*:*:*:wordpress:*:*" @@ -9837,6 +9884,9 @@ "bing-site-verification-using-meta-tag": [ "cpe:2.3:a:bing_site_verification_plugin_using_meta_tag_project:bing_site_verification_plugin_using_meta_tag:*:*:*:*:*:wordpress:*:*" ], + "binlayerpress": [ + "cpe:2.3:a:gnarf:binlayerpress:*:*:*:*:*:wordpress:*:*" + ], "birthdays-widget": [ "cpe:2.3:a:birthdays_widget_project:birthdays_widget:*:*:*:*:*:wordpress:*:*" ], @@ -9907,6 +9957,9 @@ "blog2social": [ "cpe:2.3:a:adenion:blog2social:*:*:*:*:*:wordpress:*:*" ], + "blogbuzztime-for-wp": [ + "cpe:2.3:a:gkdv:blogbuzztime_for_wp:*:*:*:*:*:wordpress:*:*" + ], "blogger-importer": [ "cpe:2.3:a:wordpress:blogger_importer:*:*:*:*:*:wordpress:*:*" ], @@ -10231,6 +10284,9 @@ "buttons-shortcode-and-widget": [ "cpe:2.3:a:otwthemes:buttons_shortcode_and_widget:*:*:*:*:*:wordpress:*:*" ], + "buybox-widget": [ + "cpe:2.3:a:getbuybox:buybox_widget:*:*:*:*:*:wordpress:*:*" + ], "buymeacoffee": [ "cpe:2.3:a:buymeacoffee:buy_me_a_coffee:*:*:*:*:*:wordpress:*:*" ], @@ -10310,6 +10366,9 @@ "cpe:2.3:a:calderaforms:caldera_forms:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:calderalabs:caldera_forms:*:*:*:*:*:wordpress:*:*" ], + "calendapp": [ + "cpe:2.3:a:margiov:calendapp:*:*:*:*:*:wordpress:*:*" + ], "calendar": [ "cpe:2.3:a:kieranoshea:calendar:*:*:*:*:*:wordpress:*:*" ], @@ -10737,7 +10796,7 @@ ], "cm-video-lesson-manager": [ "cpe:2.3:a:cminds:video_lessons_manager:*:*:*:*:*:wordpress:*:*", - "cpe:2.3:a:creativemindssolutions:video_lessons_manager_pro:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:cminds:video_lessons_manager_pro:*:*:*:*:*:wordpress:*:*" ], "cmp-coming-soon-maintenance": [ "cpe:2.3:a:niteothemes:cmp:*:*:*:*:*:wordpress:*:*" @@ -11460,6 +11519,9 @@ "custom-fields-search": [ "cpe:2.3:a:bestwebsoft:custom_fields_search:*:*:*:*:*:wordpress:*:*" ], + "custom-fields-shortcode": [ + "cpe:2.3:a:gonahkar:custom_fields_shortcode:*:*:*:*:*:wordpress:*:*" + ], "custom-font-uploader": [ "cpe:2.3:a:wbcomdesigns:custom_font_uploader:*:*:*:*:*:wordpress:*:*" ], @@ -12385,6 +12447,9 @@ "embed-comment-images": [ "cpe:2.3:a:embed_images_in_comments_project:embed_images_in_comments:*:*:*:*:*:wordpress:*:*" ], + "embed-peertube-playlist": [ + "cpe:2.3:a:info-d-74:embed_peertube_playlist:*:*:*:*:*:wordpress:*:*" + ], "embed-power-bi": [ "cpe:2.3:a:atlaspolicy:power_bi_embedded:*:*:*:*:*:wordpress:*:*" ], @@ -13943,6 +14008,9 @@ "hkdev-maintenance-mode": [ "cpe:2.3:a:helderk:maintenance_mode:*:*:*:*:*:wordpress:*:*" ], + "hl-twitter": [ + "cpe:2.3:a:dachande663:hl_twitter:*:*:*:*:*:wordpress:*:*" + ], "hmapsprem": [ "cpe:2.3:a:heroplugins:hero_maps_premium:*:*:*:*:*:wordpress:*:*" ], @@ -15305,6 +15373,9 @@ "mailtree-log-mail": [ "cpe:2.3:a:oacstudio:mailtree_log_mail:*:*:*:*:*:wordpress:*:*" ], + "mailup-auto-subscribtion": [ + "cpe:2.3:a:ilghera:mailup_auto_subscription:*:*:*:*:*:wordpress:*:*" + ], "maintenance": [ "cpe:2.3:a:webfactoryltd:maintenance:*:*:*:*:*:wordpress:*:*" ], @@ -15474,6 +15545,9 @@ "mdc-youtube-downloader": [ "cpe:2.3:a:mdc_youtube_downloader_project:mdc_youtube_downloader:*:*:*:*:*:wordpress:*:*" ], + "mdl-shortcodes": [ + "cpe:2.3:a:getbutterfly:modern_design_library:*:*:*:*:*:wordpress:*:*" + ], "media-alt-renamer": [ "cpe:2.3:a:jordymeow:media_alt_renamer:*:*:*:*:*:wordpress:*:*" ], @@ -16591,6 +16665,9 @@ "patron-button-and-widgets-by-codebard": [ "cpe:2.3:a:codebard:codebard\\'s_patron_button_and_widgets_for_patreon:*:*:*:*:*:wordpress:*:*" ], + "payflex-payment-gateway": [ + "cpe:2.3:a:payflex:payment_gateway:*:*:*:*:*:wordpress:*:*" + ], "paygreen-woocommerce": [ "cpe:2.3:a:paygreen:paygreen_-_ancienne:*:*:*:*:*:wordpress:*:*" ], @@ -16685,6 +16762,9 @@ "pepro-cf7-database": [ "cpe:2.3:a:peprodev:peprodev_cf7_database:*:*:*:*:*:wordpress:*:*" ], + "pepro-ultimate-invoice": [ + "cpe:2.3:a:pepro:peprodev_ultimate_invoice:*:*:*:*:*:wordpress:*:*" + ], "per-page-add-to": [ "cpe:2.3:a:evona:per_page_add_to_head:*:*:*:*:*:wordpress:*:*" ], @@ -16703,6 +16783,9 @@ "permalinks-customizer": [ "cpe:2.3:a:yasglobal:permalinks_customizer:*:*:*:*:*:wordpress:*:*" ], + "persian-fonts": [ + "cpe:2.3:a:danialhatami:persian_fonts:*:*:*:*:*:wordpress:*:*" + ], "persian-woocommerce": [ "cpe:2.3:a:woocommerce:persian-woocommerce:*:*:*:*:*:wordpress:*:*" ], @@ -16731,6 +16814,9 @@ "phastpress": [ "cpe:2.3:a:kiboit:phastpress:*:*:*:*:*:wordpress:*:*" ], + "philantro": [ + "cpe:2.3:a:philantro:philantro:*:*:*:*:*:wordpress:*:*" + ], "phoenix-media-rename": [ "cpe:2.3:a:phoenix_media_rename_project:phoenix_media_rename:*:*:*:*:*:wordpress:*:*" ], @@ -16813,12 +16899,18 @@ "pinterest-rss-widget": [ "cpe:2.3:a:bkmacdaddy:pinterest_rss_widget:*:*:*:*:*:wordpress:*:*" ], + "piotnet-addons-for-elementor": [ + "cpe:2.3:a:piotnet:piotnet_addons_for_elementor:*:*:*:*:*:wordpress:*:*" + ], "piotnetforms": [ "cpe:2.3:a:piotnet:piotnet_forms:*:*:*:*:*:wordpress:*:*" ], "pixabay-images": [ "cpe:2.3:a:pixabay_images_project:pixabay_images:*:*:*:*:*:wordpress:*:*" ], + "pixelstats": [ + "cpe:2.3:a:pixelstats:pixelstats:*:*:*:*:*:wordpress:*:*" + ], "pixelyoursite": [ "cpe:2.3:a:pixelyoursite:pixelyoursite:*:*:*:*:-:wordpress:*:*" ], @@ -16844,6 +16936,9 @@ "cpe:2.3:a:web-dorado:spidervplayer:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:web-dorado:web-dorado_spider_video_player:*:*:*:*:*:wordpress:*:*" ], + "playlist-for-youtube": [ + "cpe:2.3:a:info-d-74:playlist_for_youtube:*:*:*:*:*:wordpress:*:*" + ], "plexx-elementor-extension": [ "cpe:2.3:a:themeworm:plexx_elementor_extension:*:*:*:*:*:wordpress:*:*" ], @@ -16984,6 +17079,7 @@ "cpe:2.3:a:portfolio_project:portfolio:*:*:*:*:*:wordpress:*:*" ], "portfolio-elementor": [ + "cpe:2.3:a:pwrplugins:portfolio_for_elementor:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:pwrplugins:powerfolio:*:*:*:*:*:wordpress:*:*" ], "portfolio-gallery": [ @@ -17039,6 +17135,7 @@ "cpe:2.3:a:post_index_project:post_index:*:*:*:*:*:wordpress:*:*" ], "post-list-designer": [ + "cpe:2.3:a:infornweb:post_list_designer:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:infornweb:posts_list_designer_by_category:*:*:*:*:*:wordpress:*:*" ], "post-lockdown": [ @@ -17151,6 +17248,9 @@ "pre-publish-checklist": [ "cpe:2.3:a:brainstormforce:pre-publish_checklist:*:*:*:*:*:wordpress:*:*" ], + "precious-metals-chart-and-widgets": [ + "cpe:2.3:a:nfusionsolutions:precious_metals_charts_and_widgets:*:*:*:*:*:wordpress:*:*" + ], "preloader-plus": [ "cpe:2.3:a:wp-brandtheme:preloader_plus:*:*:*:*:*:wordpress:*:*" ], @@ -17212,9 +17312,15 @@ "pricing-tables-for-wpbakery-page-builder": [ "cpe:2.3:a:pricing_tables_for_wpbakery_page_builder_project:pricing_tables_for_wpbakery_page_builder:*:*:*:*:*:wordpress:*:*" ], + "pricingtable": [ + "cpe:2.3:a:pickplugins:pricing_table:*:*:*:*:*:wordpress:*:*" + ], "primary-addon-for-elementor": [ "cpe:2.3:a:nicheaddons:primary_addon_for_elementor:*:*:*:*:*:wordpress:*:*" ], + "prime-addons-for-elementor": [ + "cpe:2.3:a:nilambar:prime_addons_for_elementor:*:*:*:*:*:wordpress:*:*" + ], "prime-mover": [ "cpe:2.3:a:codexonics:prime_mover:*:*:*:*:*:wordpress:*:*" ], @@ -17264,6 +17370,9 @@ "cpe:2.3:a:awesometogi:product-category-tree:*:*:*:*:*:wordpress:*:*", "cpe:2.3:a:awesometogi:product_category_tree:*:*:*:*:*:wordpress:*:*" ], + "product-customizer-light": [ + "cpe:2.3:a:k2-service:product_customizer_light:*:*:*:*:*:wordpress:*:*" + ], "product-delivery-date-for-woocommerce-lite": [ "cpe:2.3:a:tychesoftwares:product_delivery_date_for_woocommerce:*:*:*:*:lite:wordpress:*:*" ], @@ -17316,6 +17425,9 @@ "progressive-license": [ "cpe:2.3:a:crowdfavorite:progressive_license:*:*:*:*:*:wordpress:*:*" ], + "project-notebooks": [ + "cpe:2.3:a:ptoffice:pt_project_notebooks:*:*:*:*:*:wordpress:*:*" + ], "project-source-code-download": [ "cpe:2.3:a:project-source-code-download_project:project-source-code-download:*:*:*:*:*:wordpress:*:*" ], @@ -17359,7 +17471,7 @@ "cpe:2.3:a:pubsubhubbub:websub:*:*:*:*:*:wordpress:*:*" ], "pure-chat": [ - "cpe:2.3:a:purechat:pure_chat:*:*:*:*:*:*:*:*" + "cpe:2.3:a:purechat:pure_chat:*:*:*:*:*:wordpress:*:*" ], "pure-css-circle-progress-bar": [ "cpe:2.3:a:shafayat:pure_css_circle_progress_bar:*:*:*:*:*:wordpress:*:*" @@ -22146,6 +22258,9 @@ "wp-masonry-infinite-scroll": [ "cpe:2.3:a:wporbit:wp_masonry_\\\u0026_infinite_scroll:*:*:*:*:*:wordpress:*:*" ], + "wp-media-category-management": [ + "cpe:2.3:a:de-baat:wp_media_category_management:*:*:*:*:*:wordpress:*:*" + ], "wp-media-cleaner": [ "cpe:2.3:a:wp_media_cleaner_project:wp_media_cleaner:*:*:*:*:*:wordpress:*:*" ], From 12d5544b2a5155969efac881c3a6f1ccace0fc2f Mon Sep 17 00:00:00 2001 From: Vaibhav Srivastava Date: Mon, 31 Aug 2026 22:51:05 +0530 Subject: [PATCH 161/168] docs: fix typo implmentation -> implementation (#5243) Signed-off-by: Vaibhav Srivastava --- syft/cpe/cpe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syft/cpe/cpe.go b/syft/cpe/cpe.go index c996d4ad9..04374af41 100644 --- a/syft/cpe/cpe.go +++ b/syft/cpe/cpe.go @@ -201,7 +201,7 @@ func (c Attributes) String() string { // sanitize is a modified version of WFNize function from nvdtools // that quotes all the allowed punctation chars with a slash and replaces -// spaces with underscores. It differs from the upstream implmentation as +// spaces with underscores. It differs from the upstream implementation as // it does not use the buggy nvdtools implementation, specifically the "addSlashesAt" part of the // function which stops the loop as soon as it encounters ":" a valid // character for a WFN attribute after quoting, but the way nvdtools From 77031752faf6810edf6d57c8ba798408796ea283 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:27:04 +0000 Subject: [PATCH 162/168] chore(deps): bump google.golang.org/grpc from 1.83.0 to 1.83.1 (#5245) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.0 to 1.83.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.83.0...v1.83.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.83.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c69bdab18..3fd20fbea 100644 --- a/go.mod +++ b/go.mod @@ -318,7 +318,7 @@ require ( google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect - google.golang.org/grpc v1.83.0 // indirect + google.golang.org/grpc v1.83.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/warnings.v0 v0.1.2 // indirect modernc.org/libc v1.74.4 // indirect diff --git a/go.sum b/go.sum index 917b6d66c..4e7a09535 100644 --- a/go.sum +++ b/go.sum @@ -1463,8 +1463,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= -google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From c935b71c9e28047f121a9d8892c7963644c48b0a Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Thu, 3 Sep 2026 16:09:20 -0400 Subject: [PATCH 163/168] fixed java-archive-cataloger returns unknown ver for uber-jars , fixed #5163 (#5164) Signed-off-by: Rez Moss --- syft/pkg/cataloger/java/archive_parser.go | 42 ++++++ .../java/testdata/jar-metadata/Makefile | 17 ++- .../META-INF/MANIFEST.MF | 3 + .../version.properties | 1 + .../META-INF/MANIFEST.MF | 4 + .../version.properties | 2 + .../META-INF/MANIFEST.MF | 6 + .../version.properties | 3 + .../cataloger/java/version_properties_test.go | 124 ++++++++++++++++++ 9 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/META-INF/MANIFEST.MF create mode 100644 syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/version.properties create mode 100644 syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/META-INF/MANIFEST.MF create mode 100644 syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/version.properties create mode 100644 syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/META-INF/MANIFEST.MF create mode 100644 syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/version.properties create mode 100644 syft/pkg/cataloger/java/version_properties_test.go diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index e54b73696..0ec5b0054 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -9,6 +9,7 @@ import ( "iter" "os" "path" + "regexp" "slices" "strings" @@ -301,6 +302,9 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest if version == "" { version = selectVersion(manifest, j.fileInfo) } + if version == "" { + version = j.versionFromPropertiesFile(ctx, manifest) + } if len(lics) == 0 { fileLicenses := j.getLicenseFromFileInArchive(ctx) @@ -609,6 +613,44 @@ func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) []pkg.L return identified } +// optional "v" + leading digit + version-ish chars; rejects placeholders like "${revision}" +var releaseVersionPattern = regexp.MustCompile(`^v?(\d[0-9A-Za-z.\-_+]*)$`) + +// versionFromPropertiesFile: last-resort ver from root version.properties for uber-jars built +// outside maven/gradle e.g. metabase, "tag=v0.63.5", shading can land a deps file at t same +// path, so be conservative => Main-Class jars only, "version"/"tag" keys only, version shaped values only +func (j *archiveParser) versionFromPropertiesFile(ctx context.Context, manifest *pkg.JavaManifest) string { + if manifest == nil || manifest.Main.MustGet("Main-Class") == "" { + return "" + } + + matches := j.fileManifest.GlobMatch(true, "/version.properties") + if len(matches) != 1 { + return "" + } + + contents, err := intFile.ContentsFromZip(ctx, j.archivePath, matches...) + if err != nil { + log.Debugf("unable to extract version.properties (%s): %v", j.location, err) + return "" + } + + props := map[string]string{} + for line := range strings.Lines(contents[matches[0]]) { + if k, v, ok := strings.Cut(line, "="); ok { + props[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v) + } + } + + for _, key := range []string{"version", "tag"} { + if m := releaseVersionPattern.FindStringSubmatch(props[key]); m != nil { + return m[1] + } + } + + return "" +} + func (j *archiveParser) discoverPkgsFromNestedArchives(ctx context.Context, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) { // we know that all java archives are zip formatted files, so we can use the shared zip helper return discoverPkgsFromZip(ctx, j.location, j.archivePath, j.contentPath, j.fileManifest, parentPkg, j.cfg) diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/Makefile b/syft/pkg/cataloger/java/testdata/jar-metadata/Makefile index 5cdb05b1e..5d9daaaba 100644 --- a/syft/pkg/cataloger/java/testdata/jar-metadata/Makefile +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/Makefile @@ -15,6 +15,9 @@ MULTIPLE_MATCHING = multiple-matching-2.11.5 ORG_MULTIPLE_THENAME = org.multiple-thename MICRONAUT_AOP = micronaut-aop-4.9.11 COMMONS_LANG3 = commons-lang3-3.12.0 +UBER_APP_VERSION_PROPERTIES = uber-app-version-properties +UBER_APP_VERSION_PLACEHOLDER = uber-app-version-placeholder +LIB_WITH_VERSION_PROPERTIES = lib-with-version-properties .DEFAULT_GOAL := fixtures @@ -24,7 +27,7 @@ fixtures: $(CACHE_DIR) # requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted fingerprint: $(FINGERPRINT_FILE) -$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar $(CACHE_DIR)/$(COMMONS_LANG3).jar +$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar $(CACHE_DIR)/$(COMMONS_LANG3).jar $(CACHE_DIR)/$(UBER_APP_VERSION_PROPERTIES).jar $(CACHE_DIR)/$(UBER_APP_VERSION_PLACEHOLDER).jar $(CACHE_DIR)/$(LIB_WITH_VERSION_PROPERTIES).jar $(CACHE_DIR)/$(JACKSON_CORE).jar: mkdir -p $(CACHE_DIR) @@ -62,6 +65,18 @@ $(CACHE_DIR)/$(COMMONS_LANG3).jar: mkdir -p $(CACHE_DIR) cd $(COMMONS_LANG3) && zip -r $(CACHE_PATH)/$(COMMONS_LANG3).jar . +$(CACHE_DIR)/$(UBER_APP_VERSION_PROPERTIES).jar: + mkdir -p $(CACHE_DIR) + cd $(UBER_APP_VERSION_PROPERTIES) && zip -r $(CACHE_PATH)/$(UBER_APP_VERSION_PROPERTIES).jar . + +$(CACHE_DIR)/$(UBER_APP_VERSION_PLACEHOLDER).jar: + mkdir -p $(CACHE_DIR) + cd $(UBER_APP_VERSION_PLACEHOLDER) && zip -r $(CACHE_PATH)/$(UBER_APP_VERSION_PLACEHOLDER).jar . + +$(CACHE_DIR)/$(LIB_WITH_VERSION_PROPERTIES).jar: + mkdir -p $(CACHE_DIR) + cd $(LIB_WITH_VERSION_PROPERTIES) && zip -r $(CACHE_PATH)/$(LIB_WITH_VERSION_PROPERTIES).jar . + # Jenkins plugins typically do not have the version included in the archive name, # so it is important to not include it in the generated test fixture $(CACHE_DIR)/gradle.hpi: diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/META-INF/MANIFEST.MF b/syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/META-INF/MANIFEST.MF new file mode 100644 index 000000000..826e1c76b --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Created-By: Apache Maven + diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/version.properties b/syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/version.properties new file mode 100644 index 000000000..e252ccc11 --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/lib-with-version-properties/version.properties @@ -0,0 +1 @@ +version=1.2.3 diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/META-INF/MANIFEST.MF b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/META-INF/MANIFEST.MF new file mode 100644 index 000000000..22b39ee02 --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Created-By: build.clj +Main-Class: example.core.bootstrap + diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/version.properties b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/version.properties new file mode 100644 index 000000000..ec854c15d --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-placeholder/version.properties @@ -0,0 +1,2 @@ +version=${revision} +build.date=${build_timestamp} diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/META-INF/MANIFEST.MF b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/META-INF/MANIFEST.MF new file mode 100644 index 000000000..6f80f7c3b --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/META-INF/MANIFEST.MF @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +Multi-Release: true +Created-By: build.clj +Build-Jdk-Spec: 25 +Main-Class: example.core.bootstrap + diff --git a/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/version.properties b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/version.properties new file mode 100644 index 000000000..cf3400354 --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/jar-metadata/uber-app-version-properties/version.properties @@ -0,0 +1,3 @@ +tag=v0.63.5 +hash=1dca717 +date=2026-08-05 diff --git a/syft/pkg/cataloger/java/version_properties_test.go b/syft/pkg/cataloger/java/version_properties_test.go new file mode 100644 index 000000000..2b9680cf1 --- /dev/null +++ b/syft/pkg/cataloger/java/version_properties_test.go @@ -0,0 +1,124 @@ +package java + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + intFile "github.com/anchore/syft/internal/file" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func readManifest(t *testing.T, ctx context.Context, parser *archiveParser) *pkg.JavaManifest { + t.Helper() + + matches := parser.fileManifest.GlobMatch(false, manifestGlob) + require.Len(t, matches, 1) + + contents, err := intFile.ContentsFromZip(ctx, parser.archivePath, matches...) + require.NoError(t, err) + + manifest, err := parseJavaManifest(parser.archivePath, strings.NewReader(contents[matches[0]])) + require.NoError(t, err) + + return manifest +} + +func Test_archiveParser_versionFromPropertiesFile(t *testing.T) { + tests := []struct { + name string + fixture string + want string + }{ + { + // "tag=v0.63.5" + name: "release tag from root version.properties", + fixture: "uber-app-version-properties", + want: "0.63.5", + }, + { + // clickhouse-jdbc ships "version=${revision}" must not become a version + name: "unresolved build placeholder is rejected", + fixture: "uber-app-version-placeholder", + want: "", + }, + { + // no Main-Class -> cant tell app uberjar from bundled dep + name: "library without Main-Class is rejected", + fixture: "lib-with-version-properties", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := pkgtest.Context(t) + + fixturePath := generateJavaMetadataJarFixture(t, tt.fixture, "jar") + fixture, err := os.Open(fixturePath) + require.NoError(t, err) + + parser, cleanupFn, err := newJavaArchiveParser(ctx, + file.LocationReadCloser{ + Location: file.NewLocation(fixture.Name()), + ReadCloser: fixture, + }, false, DefaultArchiveCatalogerConfig()) + defer cleanupFn() + require.NoError(t, err) + + assert.Equal(t, tt.want, parser.versionFromPropertiesFile(ctx, readManifest(t, ctx, parser))) + }) + } +} + +func Test_archiveParser_versionFromPropertiesFile_noManifest(t *testing.T) { + ctx := pkgtest.Context(t) + + fixturePath := generateJavaMetadataJarFixture(t, "uber-app-version-properties", "jar") + fixture, err := os.Open(fixturePath) + require.NoError(t, err) + + parser, cleanupFn, err := newJavaArchiveParser(ctx, + file.LocationReadCloser{ + Location: file.NewLocation(fixture.Name()), + ReadCloser: fixture, + }, false, DefaultArchiveCatalogerConfig()) + defer cleanupFn() + require.NoError(t, err) + + assert.Empty(t, parser.versionFromPropertiesFile(ctx, nil)) +} + +func Test_releaseVersionPattern(t *testing.T) { + tests := []struct { + value string + want string + }{ + {"v0.63.5", "0.63.5"}, + {"0.63.5", "0.63.5"}, + {"2.5.1", "2.5.1"}, + {"1.0.0-SNAPSHOT", "1.0.0-SNAPSHOT"}, + {"3", "3"}, + {"${revision}", ""}, + {"trunk-nightly build", ""}, + {"valhalla", ""}, + {"", ""}, + {"v", ""}, + } + + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + var got string + if m := releaseVersionPattern.FindStringSubmatch(tt.value); m != nil { + got = m[1] + } + assert.Equal(t, tt.want, got) + }) + } +} From fd7517bbba345e5b6c327cf391287a2d07110f94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:29:06 +0000 Subject: [PATCH 164/168] chore(deps): bump github.com/google/go-containerregistry (#5251) Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.21.9 to 0.22.0. - [Release notes](https://github.com/google/go-containerregistry/releases) - [Commits](https://github.com/google/go-containerregistry/compare/v0.21.9...v0.22.0) --- updated-dependencies: - dependency-name: github.com/google/go-containerregistry dependency-version: 0.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3fd20fbea..77c356547 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/goccy/go-yaml v1.19.2 github.com/gohugoio/hashstructure v0.6.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-containerregistry v0.21.9 + github.com/google/go-containerregistry v0.22.0 github.com/google/licensecheck v0.3.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.1 diff --git a/go.sum b/go.sum index 4e7a09535..5212433d4 100644 --- a/go.sum +++ b/go.sum @@ -507,8 +507,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.9 h1:F+D4uZ3iA3DLMJLfhaqMdHJbzeqm/216WGQq2dokuLs= -github.com/google/go-containerregistry v0.21.9/go.mod h1:dP5XNKcL7kMFF/TB3LfvWmVhAcv7iqkHb3oDK8aauTo= +github.com/google/go-containerregistry v0.22.0 h1:eGbCiPeYxAH/7WLLq6zTBALP0tUIFsoyRauhxXDJ53I= +github.com/google/go-containerregistry v0.22.0/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= From 3dd674c53b4e32585b28e0b1f7fe7a63a1ef0ab9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:29:14 +0000 Subject: [PATCH 165/168] chore(deps): bump anchore/sbom-action from 0.24.0 to 0.24.2 (#5250) Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.24.0 to 0.24.2. - [Release notes](https://github.com/anchore/sbom-action/releases) - [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md) - [Commits](https://github.com/anchore/sbom-action/compare/e22c389904149dbc22b58101806040fa8d37a610...3ad7283483fc7af8ff2b4ea19663c2d5ca935e26) --- updated-dependencies: - dependency-name: anchore/sbom-action dependency-version: 0.24.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 15676169f..edc571c35 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -118,7 +118,7 @@ jobs: # for updating brew formula in anchore/homebrew-syft GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} - - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 #v0.24.0 + - uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2 continue-on-error: true with: file: go.mod From 392539dc28bd489f94e2a28edf989b738b938d01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:30:25 +0000 Subject: [PATCH 166/168] chore(deps): bump github.com/CycloneDX/cyclonedx-go (#5252) Bumps [github.com/CycloneDX/cyclonedx-go](https://github.com/CycloneDX/cyclonedx-go) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/CycloneDX/cyclonedx-go/releases) - [Commits](https://github.com/CycloneDX/cyclonedx-go/compare/v0.11.0...v0.12.0) --- updated-dependencies: - dependency-name: github.com/CycloneDX/cyclonedx-go dependency-version: 0.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 77c356547..3107bb98d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.3 require ( github.com/BurntSushi/toml v1.6.0 - github.com/CycloneDX/cyclonedx-go v0.11.0 + github.com/CycloneDX/cyclonedx-go v0.12.0 github.com/Masterminds/semver/v3 v3.5.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/OneOfOne/xxhash v1.2.8 diff --git a/go.sum b/go.sum index 5212433d4..f94639fb4 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI= -github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= +github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8= +github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= @@ -929,8 +929,8 @@ github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo= -github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= -github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= +github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0= +github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= From 767c83d5131e9591a3f125dca969274399734084 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Tue, 8 Sep 2026 11:22:39 -0400 Subject: [PATCH 167/168] chore(deps): update CPE dictionary index (#5256) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .../dictionary/data/cpe-index.json | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json index 1d8d04a96..ff125a819 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json @@ -2532,6 +2532,9 @@ "@fastify/accepts-serializer": [ "cpe:2.3:a:fastify:fastify\\/accepts-serializer:*:*:*:*:*:node.js:*:*" ], + "@fastify/aws-lambda": [ + "cpe:2.3:a:fastify:fastify\\/aws-lambda:*:*:*:*:*:node.js:*:*" + ], "@fastify/express": [ "cpe:2.3:a:fastify:fastify\\/express:*:*:*:*:*:node.js:*:*" ], @@ -2544,6 +2547,9 @@ "@fastify/middie": [ "cpe:2.3:a:fastify:fastify\\/middie:*:*:*:*:*:node.js:*:*" ], + "@fastify/oauth2": [ + "cpe:2.3:a:fastify:fastify\\/oauth2:*:*:*:*:*:node.js:*:*" + ], "@fastify/rate-limit": [ "cpe:2.3:a:fastify:fastify\\/rate-limit:*:*:*:*:*:node.js:*:*" ], @@ -2905,6 +2911,9 @@ "appium-chromedriver": [ "cpe:2.3:a:appium:appium-chromedriver:*:*:*:*:*:node.js:*:*" ], + "appium-mcp": [ + "cpe:2.3:a:appium:appium-mcp:*:*:*:*:*:node.js:*:*" + ], "arcanist": [ "cpe:2.3:a:hujiang:arcanist:*:*:*:*:*:node.js:*:*" ], @@ -4412,6 +4421,9 @@ "koa-remove-trailing-slashes": [ "cpe:2.3:a:koa-remove-trailing-slashes_project:koa-remove-trailing-slashes:*:*:*:*:*:node.js:*:*" ], + "ky": [ + "cpe:2.3:a:sindresorhus:ky:*:*:*:*:*:node.js:*:*" + ], "kysely": [ "cpe:2.3:a:kysely:kysely:*:*:*:*:*:node.js:*:*" ], @@ -6766,6 +6778,9 @@ "idna": [ "cpe:2.3:a:kjd:internationalized_domain_names_in_applications:*:*:*:*:*:python:*:*" ], + "intel-transfer-learning-tool": [ + "cpe:2.3:a:intel:transfer_learning_tool:*:*:*:*:*:python:*:*" + ], "invenio-records": [ "cpe:2.3:a:inveniosoftware:invenio-records:*:*:*:*:*:*:*:*" ], @@ -16619,6 +16634,9 @@ "pagerank-tools": [ "cpe:2.3:a:mahype:pagerank_tools:*:*:*:*:*:wordpress:*:*" ], + "pagerestrict": [ + "cpe:2.3:a:sivel:page_restrict:*:*:*:*:*:wordpress:*:*" + ], "pagination": [ "cpe:2.3:a:bestwebsoft:pagination:*:*:*:*:*:wordpress:*:*" ], @@ -16759,6 +16777,9 @@ "peepso-core": [ "cpe:2.3:a:peepso:peepso:*:*:*:*:*:wordpress:*:*" ], + "peoplepond": [ + "cpe:2.3:a:solidcode:peoplepond:*:*:*:*:*:wordpress:*:*" + ], "pepro-cf7-database": [ "cpe:2.3:a:peprodev:peprodev_cf7_database:*:*:*:*:*:wordpress:*:*" ], From 9addda3da5743be99edd4971b1cfd876afb7bc9d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 9 Sep 2026 10:53:58 -0400 Subject: [PATCH 168/168] fix(test): pull elf fixture build tools from archive.debian.org (#5264) bullseye went EOL and deb.debian.org dropped it, so the elf executable fixture image stopped building and took the whole unit test job with it (fixture generation runs before any test does). main has been red on this since 2026-09-08. two separate breakages, both handled here: - the base repo moved to `archive.debian.org`, whose Release files are permanently expired, so `Acquire::Check-Valid-Until=false` is required - `bullseye-security` is worse off: its index is still served but every package in the pool behind it 404s, and it has not landed on the archive yet. dropping it is the only option right now. these are throwaway fixture build tools, so base bullseye versions are fine. verified by rebuilding the image and running the fixture `verify` target, which still reports `PASS: all security features accounted for`. Signed-off-by: Alex Goodman --- syft/file/cataloger/executable/testdata/elf/Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/syft/file/cataloger/executable/testdata/elf/Dockerfile b/syft/file/cataloger/executable/testdata/elf/Dockerfile index 46425e08f..70af60649 100644 --- a/syft/file/cataloger/executable/testdata/elf/Dockerfile +++ b/syft/file/cataloger/executable/testdata/elf/Dockerfile @@ -1,6 +1,12 @@ FROM gcc:9.5.0 -RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-dev +# bullseye is EOL: deb.debian.org dropped it, so pull from archive.debian.org (whose Release files are +# permanently expired, hence Check-Valid-Until). bullseye-security is deliberately omitted -- its index is +# still served but the pool behind it 404s, and it has not been moved to the archive yet. These are throwaway +# fixture build tools, so base bullseye package versions are fine. +RUN printf 'deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian bullseye-updates main\n' > /etc/apt/sources.list \ + && apt -o Acquire::Check-Valid-Until=false update -y \ + && apt install -y clang cmake git make m4 pkg-config zlib1g-dev ## from https://github.com/runsafesecurity/selfrando/blob/tb-v0.4.2/docs/linux-build-instructions.md #RUN git clone https://github.com/runsafesecurity/selfrando.git && \