Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plausibility test bom refs #14

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1433042
check for plausibility of bom-refs
CBeck-96 Jun 13, 2023
32d83d0
main restored
CBeck-96 Jun 13, 2023
46d60ab
Merge branch 'Festo-se:main' into main
CBeck-96 Jun 26, 2023
4f2d9af
Merge branch 'Festo-se:main' into main
CBeck-96 Jul 1, 2023
9df8f18
plausibility test
CBeck-96 Jun 13, 2023
84a9c65
added test for uniquness of bomrefs, test for tree connectivity removed
CBeck-96 Jun 20, 2023
ace7944
check for non unique bom-refs
CBeck-96 Jun 27, 2023
b9501e1
migrated non unique bom-ref to validate
CBeck-96 Jun 27, 2023
3191238
plausibility check integrated into validate
CBeck-96 Jul 1, 2023
9f40972
plausability check deleted
CBeck-96 Jul 1, 2023
c252f4f
tried solving isort errors
CBeck-96 Jul 2, 2023
5c5aa9c
isorted helper
CBeck-96 Jul 2, 2023
934e344
moved logging of validate to main
CBeck-96 Jul 2, 2023
ac27a1e
run isort then black
CBeck-96 Jul 2, 2023
28dc0fe
debugged test validate
CBeck-96 Jul 2, 2023
73ded04
another bug in test validate removed
CBeck-96 Jul 2, 2023
7f7a502
plausibility is a flag
CBeck-96 Jul 4, 2023
63f1378
used global logger
CBeck-96 Jul 4, 2023
203a9bc
Merge branch 'Festo-se:main' into plausibility_test_bom_refs
CBeck-96 Jul 4, 2023
2b660da
grammar and error
CBeck-96 Jul 4, 2023
1dce2a5
removed test sbom
CBeck-96 Jul 4, 2023
3b53d0d
Merge branch 'Festo-se:main' into plausibility_test_bom_refs
CBeck-96 Aug 3, 2023
265575a
Merge branch 'main' into plausibility_test_bom_refs
CBeck-96 Sep 4, 2023
45ebdf5
Merge branch 'main' into plausibility_test_bom_refs
italvi Sep 5, 2023
db01b63
Merge branch 'main' into plausibility_test_bom_refs
italvi Sep 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions cdxev/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
from cdxev.auxiliary.output import write_sbom
from cdxev.build_public_bom import build_public_bom
from cdxev.error import AppError, InputFileError
from cdxev.log import configure_logging
from cdxev.log import LogMessage, configure_logging
from cdxev.merge import merge
from cdxev.merge_vex import merge_vex
from cdxev.validator import validate_sbom
from cdxev.validator.warningsngreport import WarningsNgReporter

logger: logging.Logger
_STATUS_OK = 0
Expand Down Expand Up @@ -297,6 +298,16 @@ def create_validation_parser(
),
type=str,
)
parser.add_argument(
"--plausability-check",
metavar="<plausability-check>",
choices=["yes", "y"],
help=(
"y/yes if the plausibility of the bom-refs in the"
"sbom should also be checked"
),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this additional choice? Why not providing the flag plausibility-check means true?

type=str,
)

add_output_argument(parser)

Expand Down Expand Up @@ -580,27 +591,40 @@ def has_target() -> bool:


def invoke_validate(args: argparse.Namespace) -> int:
logger_validate = logging.getLogger(__name__)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why introduce a new logger and not use the existing?

sbom, file_type = read_sbom(args.input)
if args.output is None:
output = Path("./issues.json")
else:
output = args.output
report_format = args.report_format
return (
_STATUS_OK
if validate_sbom(
sbom=sbom,
input_format=file_type,
file=Path(args.input),
report_format=report_format,
output=output,
schema_type=args.schema_type,
filename_regex=args.filename_pattern,
schema_path=args.schema_path,
)
== _STATUS_OK
else _STATUS_VALIDATION_ERROR
)
sorted_errors = validate_sbom(
sbom=sbom,
input_format=file_type,
file=Path(args.input),
schema_type=args.schema_type,
filename_regex=args.filename_pattern,
schema_path=args.schema_path,
plausability_check=args.plausability_check,
)
if len(sorted_errors) == 0:
logger_validate.info("SBOM is compliant to the provided specification schema")
return 0
else:
if report_format == "warnings-ng":
warnings_ng_handler = WarningsNgReporter(Path(args.input), output)
logger_validate.addHandler(warnings_ng_handler)
for error in sorted_errors:
logger_validate.error(
LogMessage(
message="Invalid SBOM",
description=error.replace(
error[0 : error.find("has the mistake")], ""
).replace("has the mistake: ", ""),
module_name=error[0 : error.find("has the mistake") - 1],
)
)
return _STATUS_OK if sorted_errors == set() else _STATUS_VALIDATION_ERROR


def invoke_build_public_bom(args: argparse.Namespace) -> int:
Expand Down
228 changes: 228 additions & 0 deletions cdxev/validator/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from importlib import resources
from pathlib import Path

from cdxev.auxiliary.identity import ComponentIdentity
from cdxev.auxiliary.sbomFunctions import (
get_bom_refs_from_components,
get_component_by_ref,
)
from cdxev.error import AppError


Expand Down Expand Up @@ -106,3 +111,226 @@ def get_external_schema(schema_path: Path) -> tuple[dict, Path]:
"Could not load schema",
("Path to the provided schema does not exist"),
)


def get_non_unique_bom_refs(sbom: dict) -> list:
list_of_bomrefs = get_bom_refs_from_components(sbom.get("components", []))
list_of_bomrefs.append(
sbom.get("metadata", {}).get("component", {}).get("bom-ref", "")
)
non_unique_bom_refs = [
bom_ref for bom_ref in list_of_bomrefs if list_of_bomrefs.count(bom_ref) > 1
]
return list(set(non_unique_bom_refs))


def create_error_non_unique_bom_ref(reference: str, sbom: dict) -> str:
"""
Function to create an error dict for not unique bom-refs.

:param str reference: the not unique bom-ref
:param sbom : the sbom the bom-ref originates from

:return: dict with error message and error description
"""
list_of_all_components = sbom.get("components", []).copy()
list_of_all_components.append(sbom.get("metadata", {}).get("component", {}))
list_of_component_ids = []
for component in list_of_all_components:
if component.get("bom-ref", "") == reference:
list_of_component_ids.append(
ComponentIdentity.create(component, allow_unsafe=True)
)
component_description_string = ""
for component_id in list_of_component_ids:
component_description_string += f"({component_id})"
error = (
"SBOM has the mistake: found non unique bom-ref. "
+ f"The reference ({reference}) is used in several components. Those are"
+ component_description_string
)
return error


def get_errors_for_non_unique_bomrefs(sbom: dict) -> list:
list_of_non_unique_bomrefs = get_non_unique_bom_refs(sbom)
errors = []
for reference in list_of_non_unique_bomrefs:
errors.append(create_error_non_unique_bom_ref(reference, sbom))
return errors


def plausibility_check(sbom: dict) -> list:
"""
Check a sbom for plausability.
The sbom is checked for orphaned bom-refs and
components that depend on themself.


:param dict sbom: the sbom.

:return: 0 if no errors were found, 1 otherwise
:rtype: int
"""
orphaned_bom_refs_errors = check_for_orphaned_bom_refs(sbom)
dependencies_bom_refs = check_logic_of_dependencies(sbom)
united_errors = orphaned_bom_refs_errors + dependencies_bom_refs
return united_errors


def check_for_orphaned_bom_refs(sbom: dict) -> list[str]:
"""
Check a sbom for orphaned bom-refs, references that do
not correspond to any component from the sbom.

:param dict sbom: the sbom.

:return: list with the notifications of found errors
rtype: list[dict]
"""
list_of_actual_bom_refs = get_bom_refs_from_components(sbom.get("components", []))
list_of_actual_bom_refs.append(
sbom.get("metadata", {}).get("component", {}).get("bom-ref")
)
# Check if bom_refs appear in the sbom, that do not
# correspond to a component from the sbom

# check dependencies
errors = []
list_of_all_components = sbom.get("components", []).copy()
list_of_all_components.append(sbom.get("metadata", {}).get("component", {}))
for dependency in sbom.get("dependencies", []):
if dependency.get("ref", "") in list_of_actual_bom_refs:
for bom_ref in dependency.get("dependsOn", []):
if bom_ref not in list_of_actual_bom_refs:
component = get_component_by_ref(
dependency.get("ref", ""), list_of_all_components
)
id = ComponentIdentity.create(component, allow_unsafe=True)
errors.append(
create_error_orphaned_bom_ref(
bom_ref,
"dependencies-dependsOn of reference"
+ dependency.get("ref", "")
+ f" belonging to component ({id})",
)
)

else:
errors.append(
create_error_orphaned_bom_ref(dependency.get("ref", ""), "dependencies")
)

# check compositions
for composition in sbom.get("compositions", []):
for reference in composition.get("assemblies", []):
if reference not in list_of_actual_bom_refs:
errors.append(create_error_orphaned_bom_ref(reference, "compositions"))
for reference in composition.get("dependencies", []):
if reference not in list_of_actual_bom_refs:
errors.append(create_error_orphaned_bom_ref(reference, "compositions"))
# check vulnearabilities
for vulnerability in sbom.get("vulnerabilities", []):
for affected in vulnerability.get("affects", []):
if affected.get("ref", "") not in list_of_actual_bom_refs:
errors.append(
create_error_orphaned_bom_ref(
affected.get("ref", ""),
"vulnerabilitie " + vulnerability.get("id", ""),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vulnerability

)
)
return errors


def check_logic_of_dependencies(sbom: dict) -> list[str]:
"""
The function checks if the sbom contains circular dependencies,
e.g. components, that depend on themself.

:param dict sbom: the sbom
:return: list with the notifications of found errors
:rtype: list[dict]
"""
errors = []
list_of_actual_bom_refs = get_bom_refs_from_components(sbom.get("components", []))
list_of_actual_bom_refs.append(
sbom.get("metadata", {}).get("component", {}).get("bom-ref")
)
# Check for circular references in dependencies
for current_reference in list_of_actual_bom_refs:
list_of_upstream_references = get_upstream_dependency_bom_refs(
current_reference, sbom.get("dependencies", [])
)
if current_reference in list_of_upstream_references:
errors.append(create_error_circular_reference(current_reference, sbom))
return errors


def create_error_orphaned_bom_ref(reference: str, found_in: str) -> str:
"""
Function to create an error dict if orphaned bom_refs were found.

:param str reference: the orphaned reference
:param str found in: location of the orphaned sbom

:return: dict with error message and error description
"""
error = (
f"{found_in} has the mistake: found orphaned bom-ref"
f"The reference ({reference}) does not"
" correspond to any component in the sbom."
)
return error


def create_error_circular_reference(reference: str, sbom: dict) -> str:
"""
Function that creates an error dict if a selfdependend reference was found.

:param str reference: the reference that depends on itself
:param dict sbom: the sbom

:return: dict with error message and error description
"""
list_of_all_components = sbom.get("components", []).copy()
list_of_all_components.append(sbom.get("metadata", {}).get("component", {}))
component = get_component_by_ref(reference, list_of_all_components)
id = ComponentIdentity.create(component, allow_unsafe=True)
error = (
"dependencies has the mistake: found circular reference (selfdependent component)"
f"The component ({id}) depends on itself"
)
return error


def get_upstream_dependency_bom_refs(
start_reference: str, list_of_dependencies: list[dict], recursion_depth: int = 0
) -> list:
"""
Function that returns the upstream dependencies of a component,
also all the components this component depends on.

:param str start_reference: reference from which to start the recursion
return every reference this component depends on.
:param dict sbom: the sbom
:recursion_depth: parameter for the internal recursion.

:return: list with elements the component depends on.
:rtype: list[str]
"""
list_with_dependencies = []
# prevent endless recursion, max recursion number is qual to the maximal debt
# of the tree, also the number of dependencies given
if recursion_depth < len(list_of_dependencies) + 1:
recursion_depth += 1
for dependency in list_of_dependencies:
if dependency.get("ref", "") == start_reference:
for reference in dependency.get("dependsOn", ""):
list_with_dependencies.append(reference)
new_deps = get_upstream_dependency_bom_refs(
reference, list_of_dependencies, recursion_depth
)
for ref in new_deps:
if ref not in list_with_dependencies:
list_with_dependencies.append(ref)
return list_with_dependencies
43 changes: 16 additions & 27 deletions cdxev/validator/validate.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import logging
import re
from importlib import resources
from pathlib import Path

from jsonschema import Draft7Validator, FormatChecker, validators

from cdxev.log import LogMessage
from cdxev.validator.helper import open_schema, validate_filename
from cdxev.validator.warningsngreport import WarningsNgReporter

logger = logging.getLogger(__name__)
from cdxev.validator.helper import (
get_errors_for_non_unique_bomrefs,
open_schema,
plausibility_check,
validate_filename,
)

schema_path = resources.files("cdxev.auxiliary") / "schema"
with resources.as_file(schema_path) as path:
Expand All @@ -25,12 +25,11 @@ def validate_sbom(
sbom: dict,
input_format: str,
file: Path,
report_format: str,
output: Path,
schema_type: str = "default",
filename_regex: str = "",
schema_path: str = "",
) -> int:
plausability_check: str = "",
) -> set[str]:
errors = []
if input_format == "json":
sbom_schema, used_schema_path = open_schema(
Expand All @@ -42,6 +41,13 @@ def validate_sbom(
"SBOM has the mistake: file name is not according to the given regex"
)
errors.append(message)
non_unique_bom_ref_errors = get_errors_for_non_unique_bomrefs(sbom)
if plausability_check == "yes" or plausability_check == "y":
plausability_errors = plausibility_check(sbom)
for error in plausability_errors:
errors.append(error)
for error in non_unique_bom_ref_errors:
errors.append(error)
resolver = validators.RefResolver(
base_uri=f"{used_schema_path.as_uri()}/",
# according to documentation referrer has to be True, therefore ignore error from mypy
Expand Down Expand Up @@ -145,21 +151,4 @@ def validate_sbom(
else:
errors.append(error_path + error.message)
sorted_errors = set(sorted(errors))
if len(sorted_errors) == 0:
logger.info("SBOM is compliant to the provided specification schema")
return 0
else:
if report_format == "warnings-ng":
warnings_ng_handler = WarningsNgReporter(file, output)
logger.addHandler(warnings_ng_handler)
for error in sorted_errors:
logger.error(
LogMessage(
message="Invalid SBOM",
description=error.replace(
error[0 : error.find("has the mistake")], ""
).replace("has the mistake: ", ""),
module_name=error[0 : error.find("has the mistake") - 1],
)
)
return 1
return sorted_errors
Loading