Skip to content

Commit 31cd486

Browse files
a-orenclaude
andcommitted
fix(providers): handle quoted ARG defaults and skip duplicate image SBOMs
Broaden the tree-sitter ARG query to match any node type (not just unquoted_string) so quoted defaults like ARG FOO="bar" are captured. Strip surrounding quotes in collectArgs. Also skip redundant generateImageSBOM calls when the same image appears in multiple FROM lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df1dfe6 commit 31cd486

3 files changed

Lines changed: 32 additions & 3 deletions

File tree

src/providers/containerfile_parser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ export async function getFromQuery() {
2222

2323
export async function getArgQuery() {
2424
const language = await init();
25-
return new Query(language, '(arg_instruction (arg_pair name: (unquoted_string) @name default: (unquoted_string) @default))');
25+
return new Query(language, '(arg_instruction (arg_pair name: (_) @name default: (_) @default))');
2626
}

src/providers/oci_dockerfile.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ function containsExpansion(node) {
5050
return false
5151
}
5252

53+
/**
54+
* Strip surrounding single or double quotes from a string.
55+
* @param {string} text
56+
* @returns {string}
57+
* @private
58+
*/
59+
function stripQuotes(text) {
60+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
61+
return text.slice(1, -1)
62+
}
63+
return text
64+
}
65+
5366
/**
5467
* Collect ARG key-value pairs from the Dockerfile AST.
5568
* Only ARGs with a default value are collected (ARGs without defaults cannot be resolved statically).
@@ -64,7 +77,7 @@ function collectArgs(tree, argQuery) {
6477
const name = match.captures.find(c => c.name === 'name')?.node.text
6578
const defaultValue = match.captures.find(c => c.name === 'default')?.node.text
6679
if (name && defaultValue) {
67-
args.set(name, defaultValue)
80+
args.set(name, stripQuotes(defaultValue))
6881
}
6982
}
7083
return args
@@ -141,7 +154,9 @@ async function getImageSBOM(manifest, opts = {}) {
141154
const sbomByPurl = {}
142155
for (const image of images) {
143156
const imageRef = parseImageRef(image, opts)
144-
sbomByPurl[imageRef.getPackageURL().toString()] = generateImageSBOM(imageRef, opts)
157+
const purl = imageRef.getPackageURL().toString()
158+
if (purl in sbomByPurl) continue
159+
sbomByPurl[purl] = generateImageSBOM(imageRef, opts)
145160
}
146161
return {
147162
ecosystem,

test/providers/oci_dockerfile.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ suite('testing the Dockerfile/Containerfile data provider', () => {
147147
expect(result).to.deep.equal(['alpine:3.18'])
148148
})
149149

150+
/** Verifies that ARG with double-quoted default is resolved correctly. */
151+
test('resolves ARG with double-quoted default', async () => {
152+
const content = 'ARG BASE_IMAGE="ubuntu:22.04"\nFROM ${BASE_IMAGE}\n'
153+
const result = await parseAllFromImages(content)
154+
expect(result).to.deep.equal(['ubuntu:22.04'])
155+
})
156+
157+
/** Verifies that ARG with single-quoted default is resolved correctly. */
158+
test('resolves ARG with single-quoted default', async () => {
159+
const content = "ARG BASE_IMAGE='alpine:3.18'\nFROM ${BASE_IMAGE}\n"
160+
const result = await parseAllFromImages(content)
161+
expect(result).to.deep.equal(['alpine:3.18'])
162+
})
163+
150164
/** Verifies that comment lines and blank lines are ignored. */
151165
test('ignores comments and blank lines', async () => {
152166
const content = [

0 commit comments

Comments
 (0)