@@ -2,7 +2,7 @@ import fs from 'node:fs'
22
33import { generateImageSBOM , parseImageRef } from '../oci_image/utils.js'
44
5- import { getFromQuery , getParser } from './containerfile_parser.js'
5+ import { getArgQuery , getFromQuery , getParser } from './containerfile_parser.js'
66
77export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName ( ) { return 'oci' } }
88
@@ -51,43 +51,103 @@ function containsExpansion(node) {
5151}
5252
5353/**
54- * Parse the last FROM instruction from a Dockerfile using tree-sitter to extract the base image reference.
55- * In multi-stage builds, the last FROM represents the final stage.
54+ * Collect ARG key-value pairs from the Dockerfile AST.
55+ * Only ARGs with a default value are collected (ARGs without defaults cannot be resolved statically).
56+ * @param {import('web-tree-sitter').Tree } tree the parsed Dockerfile tree
57+ * @param {import('web-tree-sitter').Query } argQuery the tree-sitter query for arg_instruction nodes
58+ * @returns {Map<string, string> } map of ARG names to their default values
59+ * @private
60+ */
61+ function collectArgs ( tree , argQuery ) {
62+ const args = new Map ( )
63+ for ( const match of argQuery . matches ( tree . rootNode ) ) {
64+ const name = match . captures . find ( c => c . name === 'name' ) ?. node . text
65+ const defaultValue = match . captures . find ( c => c . name === 'default' ) ?. node . text
66+ if ( name && defaultValue ) {
67+ args . set ( name , defaultValue )
68+ }
69+ }
70+ return args
71+ }
72+
73+ /**
74+ * Resolve ARG substitutions in an image spec text using the collected ARG map.
75+ * Replaces ${VAR} and $VAR patterns with their ARG default values.
76+ * @param {string } text the image spec text potentially containing variable references
77+ * @param {Map<string, string> } args map of ARG names to their default values
78+ * @returns {string|null } the resolved text, or null if any variable could not be resolved
79+ * @private
80+ */
81+ function resolveArgs ( text , args ) {
82+ let resolved = text
83+ let hasUnresolved = false
84+ resolved = resolved . replace ( / \$ \{ ( [ ^ } ] + ) \} | \$ ( [ A - Z a - z _ ] \w * ) / g, ( _match , braced , plain ) => {
85+ const key = braced || plain
86+ if ( args . has ( key ) ) {
87+ return args . get ( key )
88+ }
89+ hasUnresolved = true
90+ return _match
91+ } )
92+ return hasUnresolved ? null : resolved
93+ }
94+
95+ /**
96+ * Parse all FROM instructions from a Dockerfile using tree-sitter to extract base image references.
97+ * In multi-stage builds, every FROM line is returned so each image can be analyzed independently.
98+ * ARG substitutions are resolved using declared default values. FROM lines with unresolvable
99+ * variables (ARGs without defaults) are silently skipped.
56100 * @param {string } manifestContent the content of the Dockerfile
57- * @returns {Promise<string> } the image reference from the last FROM instruction
58- * @throws {Error } when no FROM instruction is found or when ARG substitution is used
101+ * @returns {Promise<string[] > } array of image references from all resolvable FROM instructions
102+ * @throws {Error } when no FROM instruction is found or when no FROM lines can be resolved
59103 */
60- export async function parseFromImage ( manifestContent ) {
61- const [ parser , fromQuery ] = await Promise . all ( [ getParser ( ) , getFromQuery ( ) ] )
104+ export async function parseAllFromImages ( manifestContent ) {
105+ const [ parser , fromQuery , argQuery ] = await Promise . all ( [ getParser ( ) , getFromQuery ( ) , getArgQuery ( ) ] )
62106 const tree = parser . parse ( manifestContent )
63107 const matches = fromQuery . matches ( tree . rootNode )
64108 if ( matches . length === 0 ) {
65109 throw new Error ( 'No FROM line found in Dockerfile' )
66110 }
67- const lastMatch = matches [ matches . length - 1 ]
68- const imageSpec = lastMatch . captures . find ( c => c . name === 'image' ) . node
69- if ( containsExpansion ( imageSpec ) ) {
70- throw new Error ( 'Dockerfile uses ARG substitution in FROM line — cannot resolve variable references' )
111+ const args = collectArgs ( tree , argQuery )
112+ const images = [ ]
113+ for ( const match of matches ) {
114+ const imageSpec = match . captures . find ( c => c . name === 'image' ) . node
115+ if ( containsExpansion ( imageSpec ) ) {
116+ const resolved = resolveArgs ( imageSpec . text , args )
117+ if ( resolved != null ) {
118+ images . push ( resolved )
119+ }
120+ continue
121+ }
122+ images . push ( imageSpec . text )
123+ }
124+ if ( images . length === 0 ) {
125+ throw new Error ( 'Dockerfile uses ARG substitution in all FROM lines — cannot resolve variable references' )
71126 }
72- return imageSpec . text
127+ return images
73128}
74129
75130/**
76- * Generate an image SBOM from a Dockerfile manifest using syft.
131+ * Generate image SBOMs for all FROM instructions in a Dockerfile manifest using syft.
132+ * Returns a batch result with a purl-to-SBOM map suitable for the batch-analysis endpoint.
77133 * @param {string } manifest path to the Dockerfile
78134 * @param {{} } [opts={}] optional various options to pass along the application
79- * @returns {Promise<{ecosystem: string, content: string, contentType: string}> }
135+ * @returns {Promise<{ecosystem: string, content: string, contentType: string, batch: boolean }> }
80136 * @private
81137 */
82138async function getImageSBOM ( manifest , opts = { } ) {
83139 const manifestContent = fs . readFileSync ( manifest , 'utf-8' )
84- const image = await parseFromImage ( manifestContent )
85- const imageRef = parseImageRef ( image , opts )
86- const sbom = generateImageSBOM ( imageRef , opts )
140+ const images = await parseAllFromImages ( manifestContent )
141+ const sbomByPurl = { }
142+ for ( const image of images ) {
143+ const imageRef = parseImageRef ( image , opts )
144+ sbomByPurl [ imageRef . getPackageURL ( ) . toString ( ) ] = generateImageSBOM ( imageRef , opts )
145+ }
87146 return {
88147 ecosystem,
89- content : JSON . stringify ( sbom ) ,
90- contentType : 'application/vnd.cyclonedx+json'
148+ content : JSON . stringify ( sbomByPurl ) ,
149+ contentType : 'application/vnd.cyclonedx+json' ,
150+ batch : true
91151 }
92152}
93153
0 commit comments