Skip to content

Commit 465e1da

Browse files
bluesentinelsecMichael Long
andauthored
[v1.3.0] Only trigger vuln threshold on fixable vulns (#122)
* Add --threshold-fixable-only to CLI * implemented business logic * changed 'threshold_fixable_only' from str to bool * Added more test coverage and CLI refinements * debugging failing unit test * test threshold-fixable-only in workflow * test threshold-fixable-only in workflow * debugging CI/CD * debugging CI/CD * debugging * debugging * debugging * debugging * removed debug log showing CLI arguments * add missing argument, fixed_vuln_counts * simplify get_fixed_vuln_counts() return values * refactor return types in get_scan_result() * refactor * refine get_fixed_vuln_counts() * update test_get_fixed_vuln_counts() * testing case sensitivity * revert 'TRUE' to 'true' * use debug log when vuln doesnt have rating * integrate --show-only-fixable-vulns (part 1) * integrate only show fixable vulns * test example workflows * fix CLI input arguments * remove leading '-' character for conditional inclusion * add a no-op CLI arg (workaround) * enable new arguments in workflows * fix failing test * update workflows for prod --------- Co-authored-by: Michael Long <mlongii@amazon.com>
1 parent 9058e37 commit 465e1da

File tree

6 files changed

+212
-102
lines changed

6 files changed

+212
-102
lines changed

.github/workflows/test_vuln_thresholds.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ jobs:
4545
low_threshold: 1
4646
other_threshold: 1
4747
sbomgen_version: "latest"
48+
threshold_fixable_only: true
49+
show_only_fixable_vulns: true
4850

4951
- name: Fail if vulnerability threshold is exceeded
5052
run: if [[ ${{ steps.inspector.outputs.vulnerability_threshold_exceeded }} != "1" ]]; then echo "test failed"; else echo "test passed"; fi
51-
52-
# TODO: handle failure case

action.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ inputs:
110110
description: "Specifies the OS and CPU arch of the container image you wish to scan. Valid inputs are of the form 'os/cpu/variant' for example, 'linux/amd64', 'linux/arm64/v8', etc. If no platform is specified, the system will use the same platform as the host that is performing the scan. This argument only affects container image scans. Requires inspector-sbomgen 1.5.1 or later."
111111
required: False
112112

113+
threshold_fixable_only:
114+
description: 'If set to true, only count vulnerabilities with a fix towards threshold exceeded condition.'
115+
required: False
116+
default: false
117+
type: boolean
118+
119+
show_only_fixable_vulns:
120+
description: "If set to true, this action will show only fixed vulnerabilities in the GitHub Actions step summary page. All vulnerability metadata is still retained in the raw Inspector scan files."
121+
required: False
122+
default: false
123+
type: boolean
124+
113125
outputs:
114126
artifact_sbom:
115127
description: "The filepath to the artifact's software bill of materials."
@@ -148,6 +160,8 @@ runs:
148160
- --out-dockerfile-scan-md=${{ inputs.output_inspector_dockerfile_scan_path_markdown }}
149161
- --sbomgen-version=${{ inputs.sbomgen_version }}
150162
- --thresholds
163+
- ${{ inputs.threshold_fixable_only == 'true' && '--threshold-fixable-only' || '--no-op' }}
164+
- ${{ inputs.show_only_fixable_vulns == 'true' && '--show-only-fixable-vulns'|| '--no-op' }}
151165
- --critical=${{ inputs.critical_threshold }}
152166
- --high=${{ inputs.high_threshold }}
153167
- --medium=${{ inputs.medium_threshold }}

entrypoint/entrypoint/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,19 @@ def init(sys_argv=None) -> argparse.Namespace:
5050
help="Specifies one or more files and/or directories that should NOT be inventoried.")
5151
parser.add_argument("--timeout", type=str, default="600",
5252
help="The amount of time in seconds that inspector-sbomgne will run. When this timeout is exceeded, sbomgen will gracefully conclude and present any findings discovered up to that point.")
53-
parser.add_argument("--show-only-fixed-vulnerabilities", action="store_true", help="If set, this program will only show fixed vulnerabilities in the GitHub Actions job summary page.")
53+
parser.add_argument("--show-only-fixable-vulns", action="store_true", default=False,
54+
help="Only show fixed vulnerabilities in the GitHub Actions job summary page.")
55+
parser.add_argument("--threshold-fixable-only", action="store_true", default=False,
56+
help="Only count vulnerabilities with a fix towards threshold exceeded condition.")
5457

5558
parser.add_argument("--platform", type=str,
5659
help="Specifies the OS and CPU arch of the container image you wish to scan. Valid inputs are "
5760
"of the form 'os/cpu/variant' for example, 'linux/amd64', 'linux/arm64/v8', etc. If no platform is "
5861
"specified, the system will use the same platform as the host that is performing the "
5962
"scan. This argument only affects container image scans. Requires inspector-sbomgen "
6063
"1.5.1 or later.")
64+
parser.add_argument("--no-op", action="store_true", default=False,
65+
help="A no operation argument, used as the default from the GitHub Actions caller when boolean arguments are not set. This is a workaround because GitHub Actions doesn't have a clean way to invoke or not invoke action='store_true' arguments")
6166

6267
args = ""
6368
if sys_argv:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class FixedVulns:
6+
criticals: int
7+
highs: int
8+
mediums: int
9+
lows: int
10+
others: int

entrypoint/entrypoint/orchestrator.py

Lines changed: 101 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import logging
44
import os
55
import platform
6+
import re
67
import shutil
78
import sys
89
import tempfile
9-
import re
10+
import typing
1011

11-
from entrypoint import dockerfile, executor, exporter, installer, pkg_vuln
12+
from entrypoint import dockerfile, executor, exporter, installer, pkg_vuln, fixed_vulns
1213

1314

1415
def execute(args) -> int:
@@ -26,12 +27,13 @@ def execute(args) -> int:
2627
set_github_actions_output('inspector_scan_results', args.out_scan)
2728

2829
logging.info("tallying vulnerabilities")
29-
succeeded, scan_result = get_scan_result(args)
30+
succeeded, scan_result, fixed_vuln_counts = get_scan_result(args)
3031
require_true(succeeded, "unable to tally vulnerabilities")
3132

3233
print_vuln_count_summary(scan_result)
3334

34-
set_flag_if_vuln_threshold_exceeded(args, scan_result)
35+
vuln_counts = fixed_vuln_counts if args.threshold_fixable_only else scan_result
36+
set_env_var_if_vuln_threshold_exceeded(args, vuln_counts)
3537

3638
write_pkg_vuln_report_csv(args.out_scan_csv, scan_result)
3739
set_github_actions_output('inspector_scan_results_csv', args.out_scan_csv)
@@ -166,7 +168,7 @@ def invoke_sbomgen(args) -> int:
166168

167169
elif "archive" in args.artifact_type.lower():
168170
args.artifact_type = "archive"
169-
path_arg = "--path"
171+
path_arg = "--path"
170172

171173
else:
172174
logging.error(
@@ -201,7 +203,8 @@ def invoke_sbomgen(args) -> int:
201203
if args.platform:
202204
platform_arg = args.platform.lower()
203205
if not is_valid_container_platform(platform_arg):
204-
logging.fatal(f"received invalid container image platform: '{args.platform}'. Platform should be of the form 'os/cpu/variant' such as 'linux/amd64' or 'linux/arm64/v8'")
206+
logging.fatal(
207+
f"received invalid container image platform: '{args.platform}'. Platform should be of the form 'os/cpu/variant' such as 'linux/amd64' or 'linux/arm64/v8'")
205208
sbomgen_args.append("--platform")
206209
sbomgen_args.append(platform_arg)
207210

@@ -232,40 +235,45 @@ def invoke_inspector_scan(src_sbom, dst_scan):
232235
return ret
233236

234237

235-
def get_scan_result(args) -> tuple[bool, exporter.InspectorScanResult]:
236-
if args.show_only_fixed_vulnerabilities == True:
237-
succeeded, criticals, highs, mediums, lows, others = get_fixed_vuln_counts(args.out_scan)
238-
if succeeded is False:
239-
return False, None
240-
else:
241-
succeeded, criticals, highs, mediums, lows, others = get_vuln_counts(args.out_scan)
242-
if succeeded is False:
243-
return False, None
238+
def get_scan_result(args) -> tuple[bool, exporter.InspectorScanResult, fixed_vulns.FixedVulns]:
239+
scan_result = exporter.InspectorScanResult(vulnerabilities=[pkg_vuln.Vulnerability()])
240+
fixed_vulns_counts = fixed_vulns.FixedVulns(criticals=0, highs=0, mediums=0, lows=0, others=0)
241+
242+
succeeded, fixed_vulns_counts = get_fixed_vuln_counts(
243+
args.out_scan)
244+
if succeeded is False:
245+
return False, scan_result, fixed_vulns_counts
246+
247+
succeeded, criticals, highs, mediums, lows, others = get_vuln_counts(args.out_scan)
248+
if succeeded is False:
249+
return False, scan_result, fixed_vulns_counts
244250

245251
try:
246252
with open(args.out_scan, "r") as f:
247253
inspector_scan = json.load(f)
248254
vulns = pkg_vuln.parse_inspector_scan_result(inspector_scan)
249-
if args.show_only_fixed_vulnerabilities:
250-
for vuln in vulns:
251-
if vuln.fixed_ver == "null":
252-
vulns.remove(vuln)
255+
253256
except Exception as e:
254257
logging.error(e)
255-
return False, None
256-
258+
return False, scan_result, fixed_vulns_counts
259+
260+
if args.show_only_fixable_vulns:
261+
for vuln in vulns:
262+
if vuln.fixed_ver == "null":
263+
vulns.remove(vuln)
264+
257265
scan_result = exporter.InspectorScanResult(
258266
vulnerabilities=vulns,
259267
artifact_name=args.artifact_path,
260268
artifact_type=args.artifact_type,
261-
criticals=criticals,
262-
highs=highs,
263-
mediums=mediums,
264-
lows=lows,
265-
others=others
269+
criticals=str(criticals),
270+
highs=str(highs),
271+
mediums=str(mediums),
272+
lows=str(lows),
273+
others=str(others)
266274
)
267275

268-
return succeeded, scan_result
276+
return succeeded, scan_result, fixed_vulns_counts
269277

270278

271279
def set_github_actions_output(key, value):
@@ -276,6 +284,10 @@ def set_github_actions_output(key, value):
276284
logging.info(f"setting github actions output: {key}:{value}")
277285
os.system(f'echo "{key}={value}" >> "$GITHUB_OUTPUT"')
278286

287+
# set an ENV VAR so that outputs
288+
# look the same locally or on GitHub Actions
289+
os.environ[key] = str(value)
290+
279291
return
280292

281293

@@ -340,78 +352,83 @@ def get_vuln_counts(inspector_scan_path: str) -> tuple[bool, int, int, int, int,
340352

341353
return True, criticals, highs, mediums, lows, others
342354

343-
def get_fixed_vuln_counts(inspector_scan_path: str) -> tuple[bool, int, int, int, int, int]:
344-
criticals_fixed = 0
345-
highs_fixed = 0
346-
mediums_fixed = 0
347-
lows_fixed = 0
348-
others_fixed = 0
355+
356+
def get_fixed_vuln_counts(inspector_scan_path: str) -> tuple[bool, fixed_vulns.FixedVulns]:
357+
fixed_vulns_counts = fixed_vulns.FixedVulns(criticals=0, highs=0, mediums=0, lows=0, others=0)
349358

350359
scan_contents = ""
351360
try:
352361
with open(inspector_scan_path, 'r') as f:
353362
scan_contents = json.load(f)
354363
except Exception as e:
355364
logging.error(e)
356-
return False, criticals, highs, mediums, lows, others
365+
return False, fixed_vulns_counts
357366

358367
# find the sbom->metadata->properties object
359368
scan_contents = scan_contents.get("sbom")
360369
if scan_contents is None:
361370
logging.error(
362371
f"expected Inspector scan results with 'sbom' as root object, but it was not found in file {inspector_scan_path}")
363-
return False, criticals, highs, mediums, lows, others
372+
return False, fixed_vulns_counts
364373

365374
vulnerabilities = scan_contents.get("vulnerabilities")
366375

367376
if vulnerabilities is None:
368377
# no vulnerabilities found
369-
return True, criticals_fixed, highs_fixed, mediums_fixed, lows_fixed, others_fixed
370-
378+
return True, fixed_vulns_counts
379+
380+
is_fix_available_key = "amazon:inspector:sbom_scanner:fixed_version:comp-"
371381
for vuln in vulnerabilities:
372382
props = vuln.get("properties")
373383
if props is None:
374-
logging.error(f"expected vulnerability with 'properties' key but none was found in file {inspector_scan_path}")
384+
logging.debug(
385+
f"expected vulnerability with 'properties' key but none was found in file {inspector_scan_path}")
375386
continue
376387

377388
for prop in props:
378389
name = prop.get("name")
379390
if name is None:
380-
logging.error(f"expected vulnerability with 'name' key but none was found in file {inspector_scan_path}")
381391
continue
382-
if "amazon:inspector:sbom_scanner:fixed_version:comp-" in name:
383-
rating = vuln.get("ratings")
384-
if rating is None:
385-
logging.error(f"expected vulnerability with 'rating' key but none was found in file {inspector_scan_path}")
392+
if is_fix_available_key not in name:
393+
continue
394+
395+
# vuln has an available fix
396+
rating = vuln.get("ratings")
397+
if rating is None:
398+
logging.error(
399+
f"expected vulnerability with 'rating' key but none was found in file {inspector_scan_path}")
400+
continue
401+
402+
previous_score = 0
403+
previous_severity = ""
404+
for each_rating in rating:
405+
severity = each_rating.get("severity")
406+
if severity is None:
407+
logging.error(
408+
f"expected vulnerability with 'severity' key but none was found in file {inspector_scan_path}")
409+
continue
410+
score = each_rating.get("score")
411+
if score is None:
412+
logging.debug(
413+
f"expected vulnerability with 'score' key but none was found in file {inspector_scan_path}")
386414
continue
415+
if previous_score < score:
416+
previous_score = score
417+
previous_severity = severity
418+
419+
if previous_severity == "critical":
420+
fixed_vulns_counts.criticals += 1
421+
elif previous_severity == "high":
422+
fixed_vulns_counts.highs += 1
423+
elif previous_severity == "medium":
424+
fixed_vulns_counts.mediums += 1
425+
elif previous_severity == "low":
426+
fixed_vulns_counts.lows += 1
427+
else:
428+
fixed_vulns_counts.others += 1
429+
430+
return True, fixed_vulns_counts
387431

388-
previous_score = 0
389-
previous_severity = ""
390-
for each_rating in rating:
391-
severity = each_rating.get("severity")
392-
if severity is None:
393-
logging.error(f"expected vulnerability with 'severity' key but none was found in file {inspector_scan_path}")
394-
continue
395-
score = each_rating.get("score")
396-
if score is None:
397-
logging.error(f"expected vulnerability with 'score' key but none was found in file {inspector_scan_path}")
398-
continue
399-
if previous_score < score:
400-
previous_score = score
401-
previous_severity = severity
402-
403-
if previous_severity == "none":
404-
others_fixed += 1
405-
elif previous_severity == "critical":
406-
criticals_fixed += 1
407-
elif previous_severity == "high":
408-
highs_fixed += 1
409-
elif previous_severity == "medium":
410-
mediums_fixed += 1
411-
elif previous_severity == "low":
412-
lows_fixed += 1
413-
414-
return True, criticals_fixed, highs_fixed, mediums_fixed, lows_fixed, others_fixed
415432

416433
def install_sbomgen(args):
417434
os_name = platform.system()
@@ -452,12 +469,14 @@ def write_pkg_vuln_report_markdown(out_scan_markdown, scan_result: exporter.Insp
452469
return markdown
453470

454471

455-
def set_flag_if_vuln_threshold_exceeded(args, scan_result: exporter.InspectorScanResult):
456-
is_exceeded = exceeds_threshold(scan_result.criticals, args.critical,
457-
scan_result.highs, args.high,
458-
scan_result.mediums, args.medium,
459-
scan_result.lows, args.low,
460-
scan_result.others, args.other)
472+
def set_env_var_if_vuln_threshold_exceeded(args,
473+
vuln_counts: typing.Union[
474+
exporter.InspectorScanResult, fixed_vulns.FixedVulns]):
475+
is_exceeded = exceeds_threshold(vuln_counts.criticals, args.critical,
476+
vuln_counts.highs, args.high,
477+
vuln_counts.mediums, args.medium,
478+
vuln_counts.lows, args.low,
479+
vuln_counts.others, args.other)
461480

462481
if is_exceeded and args.thresholds:
463482
set_github_actions_output('vulnerability_threshold_exceeded', 1)
@@ -471,19 +490,19 @@ def exceeds_threshold(criticals, critical_threshold,
471490
lows, low_threshold,
472491
others, other_threshold) -> bool:
473492
is_threshold_exceed = False
474-
if 0 < critical_threshold <= criticals:
493+
if 0 < critical_threshold <= int(criticals):
475494
is_threshold_exceed = True
476495

477-
if 0 < high_threshold <= highs:
496+
if 0 < high_threshold <= int(highs):
478497
is_threshold_exceed = True
479498

480-
if 0 < medium_threshold <= mediums:
499+
if 0 < medium_threshold <= int(mediums):
481500
is_threshold_exceed = True
482501

483-
if 0 < low_threshold <= lows:
502+
if 0 < low_threshold <= int(lows):
484503
is_threshold_exceed = True
485504

486-
if 0 < other_threshold <= others:
505+
if 0 < other_threshold <= int(others):
487506
is_threshold_exceed = True
488507

489508
return is_threshold_exceed
@@ -533,6 +552,7 @@ def require_true(expr: bool, msg: str):
533552
logging.error(msg)
534553
exit(1)
535554

555+
536556
def is_valid_container_platform(img_platform):
537557
# regex for detecting 'os/cpu/variant'
538558
# os/cpu are required whereas variant is optional

0 commit comments

Comments
 (0)