Skip to content

Commit

Permalink
feat: support running the analysis with SBOM and the main software co…
Browse files Browse the repository at this point in the history
…mponent with no repository (#165)

Core engine:
* For all software components (main target and dependencies), the analysis will not be skipped if the repository URL is not found.
* Collect and run the analysis for dependencies from the SBOM (if provided) even when the repository URL is not available for the main target.
HTML reports:
* Display a small message in the Target Information section when the repository is not available.
* Collapse the check report table when all checks fail.

Signed-off-by: Trong Nhan Mai <[email protected]>
  • Loading branch information
tromai authored Oct 4, 2023
1 parent fcdf22d commit abd2c31
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 58 deletions.
2 changes: 2 additions & 0 deletions docs/source/pages/cli_usage/action_verify-policy.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.. Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved.
.. Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
.. _verify-policy-action-cli:

=============
Verify Policy
=============
Expand Down
17 changes: 17 additions & 0 deletions docs/source/pages/using.rst
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,23 @@ With the example above, the generated output reports can be seen here:
- `micronaut-core.html <../_static/examples/micronaut-projects/micronaut-core/analyze_with_sbom/micronaut-core.html>`__
- `micronaut-core.json <../_static/examples/micronaut-projects/micronaut-core/analyze_with_sbom/micronaut-core.json>`__

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Analyzing dependencies in the SBOM without the main software component
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

In the case where the repository URL of the main software component is not available (e.g. the repository is in a private domain where Macaron cannot access),
Macaron can still run the analysis on the dependencies listed in the SBOM.
To do that, you must first create a PURL to present the main software component. This is so that this software component could be referenced later in the :ref:`verify-policy <verify-policy-action-cli>` command.
For example: ``pkg:private_domain.com/org/name``.

Then the analysis can be run with:

.. code-block:: shell
./run_macaron.sh analyze -purl pkg:private_domain.com/org/name -sbom <path_to_sbom>
With ``path_to_sbom`` is the path to the SBOM you want to use.

.. _more-deps:

'''''''''''''''''''''''''''
Expand Down
14 changes: 13 additions & 1 deletion scripts/dev_scripts/integration_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ python $COMPARE_DEPS $DEP_RESULT $DEP_EXPECTED || log_fail
python $COMPARE_JSON_OUT $JSON_RESULT $JSON_EXPECTED || log_fail

echo -e "\n----------------------------------------------------------------------------------"
echo "apache/maven: Analyzing the repo path, the branch name and the commit digest with dependency resolution using a CycloneDx SBOM."
echo "apache/maven: Analyzing using a CycloneDx SBOM with target repo path"
echo -e "----------------------------------------------------------------------------------\n"
SBOM_FILE=$WORKSPACE/tests/dependency_analyzer/cyclonedx/resources/apache_maven_root_sbom.json
DEP_EXPECTED=$WORKSPACE/tests/dependency_analyzer/expected_results/apache_maven_with_sbom_provided.json
Expand All @@ -183,6 +183,18 @@ $RUN_MACARON analyze -rp https://github.com/apache/maven -b master -d 6767f2500f

python $COMPARE_DEPS $DEP_RESULT $DEP_EXPECTED || log_fail


echo -e "\n----------------------------------------------------------------------------------"
echo "apache/maven: Analyzing using a CycloneDx SBOM file of a software component whose repository is not available."
echo -e "----------------------------------------------------------------------------------\n"
SBOM_FILE=$WORKSPACE/tests/dependency_analyzer/cyclonedx/resources/private_mirror_apache_maven.json
DEP_EXPECTED=$WORKSPACE/tests/dependency_analyzer/expected_results/private_mirror_apache_maven.json
DEP_RESULT=$WORKSPACE/output/reports/private_domain_com/apache/maven/dependencies.json

$RUN_MACARON analyze -purl pkg:private_domain.com/apache/maven -sbom "$SBOM_FILE" || log_fail

python $COMPARE_DEPS $DEP_RESULT $DEP_EXPECTED || log_fail

# Analyze micronaut-projects/micronaut-core.
echo -e "\n=================================================================================="
echo "Run integration tests with configurations for micronaut-projects/micronaut-core..."
Expand Down
12 changes: 8 additions & 4 deletions src/macaron/output_reporter/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,17 @@ def get_dict(self) -> dict:
dict
The dictionary representation of this record.
"""
has_passing_check = False
if self.context:
for res in self.context.check_results.values():
if res["result_type"] == CheckResultType.PASSED:
has_passing_check = True
break

result = {
"metadata": {
"timestamps": datetime.now().isoformat(sep=" ", timespec="seconds"),
"has_passing_check": has_passing_check,
},
"target": self.context.get_dict() if self.context else {},
"dependencies": self.get_dep_summary(),
Expand Down Expand Up @@ -303,8 +311,4 @@ def __str__(self) -> str:
for mesg in mesg_list:
output = "".join([output, f"- {mesg}\n"])

for record in self.get_records():
if not record.context:
continue

return output
47 changes: 17 additions & 30 deletions src/macaron/output_reporter/templates/base_template.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- Copyright (c) 2022 - 2022, Oracle and/or its affiliates. All rights reserved. -->
<!-- Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. -->
<!-- Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. -->

<!DOCTYPE html>
Expand Down Expand Up @@ -255,26 +255,30 @@
padding-left: 2em
}

.caret {
/*
The reason why we need to create a separate .toggler class is because all .caret class are set binded
to the listener for extending/collapsing the provenance fields.
*/
.caret, .toggler {
cursor: pointer;
user-select: none;
}

/* Create the caret using a unicode symbol, and style it. */
.caret::before {
.caret::before, .toggler::before {
content: "\25B8";
color: black;
display: inline-block;
margin-right: 6px;
}

/* Rotate the caret icon when clicked on. */
.caret-down::before {
.caret-down::before, .toggler-extend::before {
transform: rotate(90deg);
}

/* Hide the nested list. */
.nested-ul {
.nested-ul, .hidden {
display: none;
}

Expand Down Expand Up @@ -609,31 +613,7 @@
</div>
</header>

<div class="table_caption">Target information</div>
{% block target_info%}
{% endblock %}

<div class='space_divider'></div>

<div class="table_caption">Provenance summary</div>
{% block prov_justification %}
{% endblock %}

<button class="fancy-button" onclick="expandAll()">Expand All</button>
<button class="fancy-button" onclick="collapseAll()">Collapse All</button>

{% block prov_summary %}
{% endblock %}

<div class='space_divider'></div>

<div class="table_caption">Reports for Macaron checks</div>
{% block checks_report %}
{% endblock%}

<div class='space_divider'></div>

{% block deps_result_section %}
{% block content %}
{% endblock %}

<footer class='footer'>
Expand Down Expand Up @@ -696,6 +676,13 @@
});
}

// Add a listener to the caret for extending/collapsing the check status table.
let check_status_toggler = document.getElementById("check_report_title");
check_status_toggler.addEventListener("click", function() {
this.classList.toggle("toggler-extend");
document.getElementById("check_report_content").classList.toggle("hidden");
});

// When loaded, expand all CI services.<n> elements
setExpandState(document.querySelectorAll(".tree-view-nested-list > * > .caret"), true);
setExpandState(document.querySelectorAll(".tree-view-nested-list > * > * > * > .caret"), true);
Expand Down
81 changes: 62 additions & 19 deletions src/macaron/output_reporter/templates/macaron.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,49 +168,92 @@

{#
This section is where we fill in the blocks defined in base_template.html.
Note that the input context dictionary will have the format:
{data = <data_from_html_reporter}
The data for this template is passed in as a Python dictionary by the HTMLReporter. We can access those data by
specifying the keys to the required value field.
For example, if the data from HTMLReporter is:
{
"metadata": {
"boo": 1,
"foo": 2
}
}
Within the Jinja2 template, ``{{ metadata.boo }}`` will be rendered as ``1``
#}

{% block timestamps %}
{{ metadata.timestamps | indent(12, first=true) }}
{% endblock%}
{% endblock %}

{% block content %}
<div class="table_caption">Target information</div>
{% if not target.info.remote_path %}
{#
Display the related section only the remote path is available. The remote path for a target software component
is not available when Macaron cannot resolve it from the user provided PURL.
#}
<div id= "prov_justification">
Manual configuration required. Could not find SCM URL.
</div>
{% endif %}

{% block target_info %}
{{ render_target_info_table(target.info) | indent(4, first=true) }}
{% endblock %}
{{ render_target_info_table(target.info) | indent(4) }}

<div class='space_divider'></div>

{% block prov_justification %}
<div class="table_caption">Provenance summary</div>
<div id= "prov_justification">
{% if target.provenances.is_inferred == true %}
Could not find a provenance for this repository. Below is what Macaron has inferred.
{% else %}
This is the provenance found for this repository.
{% endif %}
</div>
{% endblock %}

<button class="fancy-button" onclick="expandAll()">Expand All</button>
<button class="fancy-button" onclick="collapseAll()">Collapse All</button>
{{ render_tree_view_nested_list(target.provenances.content) | indent(4) }}

{% block prov_summary %}
{{ render_tree_view_nested_list(target.provenances.content) | indent(4, first=true) }}
{% endblock %}

<div class='space_divider'></div>

{% block checks_report %}
{{ render_checks_report(target.checks.results) | indent(4, first=true) }}
{% endblock %}
{#
When there is no passing check for this software component, we hide the check report table when the viewer
first open the HTML file. However, the viewer could click the `toggler` to expand the table if they want to see it. To achieve this, there are two component we need to handle:
- The toggler, which is the symbol that indicate the Extended or Collapsed state of the check report table:
The class ``toggler-extend`` will apply the CSS rule to rotate the toggler symbol indicating the "Extend" state.
If ``toggler-extend`` is not set, the toggler will be in the Collapsed state.
- The ``check_report_content`` div, which contains the check report table: The HTML class ``hidden`` will set ``display: none`` to this div and hide the check report table.
#}
{% if metadata.has_passing_check %}
{#
Set the default state to Extend if there is a passing check.
#}
<div class="table_caption toggler toggler-extend" id="check_report_title">Reports for Macaron checks</div>
<div id="check_report_content">
{% else %}
{#
Set the default state to Collapsed if there is no passing check.
#}
<div class="table_caption toggler" id="check_report_title">Reports for Macaron checks</div>
<div id="check_report_content" class="hidden">
{% endif %}
{{ render_checks_report(target.checks.results) | indent(8) }}
</div>

<div class='space_divider'></div>

{% block deps_result_section %}
{% if dependencies.analyzed_deps != 0 %}
{#
Display the dependencies data if it's available:
- The total number of dependencies and unique repositories include in the analysis
- Dependency results (The number of dependencies pass each check)
- The status for each dependency
#}
{% if dependencies.analyzed_deps != 0 %}
<div class="table_caption" id="deps_result_title">Dependency results</div>
<div class="table_sub_caption">
{{ dependencies.analyzed_deps }} dependencies that map to {{ dependencies.unique_dep_repos }} unique repositories have been successfully analyzed.
{{ dependencies.analyzed_deps }} dependencies have been found.
</div>
{{ render_dep_summary(dependencies.checks_summary) | indent(4) }}
<div class="space_divider"></div>
{{ render_dep_status(dependencies.dep_status) | indent(4) }}
{% endif %}
{% endif %}
{% endblock %}
6 changes: 2 additions & 4 deletions src/macaron/slsa_analyzer/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,15 @@ def run(self, user_config: dict, sbom_path: str = "", skip_deps: bool = False) -
logger.info("Start analyzing the dependencies.")
for config in deps_config:
dep_status: SCMStatus = config.get_value("available")
if dep_status != SCMStatus.AVAILABLE:
if dep_status == SCMStatus.DUPLICATED_SCM:
dep_record: Record = Record(
record_id=config.get_value("id"),
description=config.get_value("note"),
pre_config=config,
status=config.get_value("available"),
)
report.add_dep_record(dep_record)
if dep_status == SCMStatus.DUPLICATED_SCM:
duplicated_scm_records.append(dep_record)

duplicated_scm_records.append(dep_record)
continue
dep_record = self.run_single(config, analysis, report.record_mapping)
report.add_dep_record(dep_record)
Expand Down
Loading

0 comments on commit abd2c31

Please sign in to comment.