feat(output): Add HTML output Prowler (#4005)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Pedro Martín
2024-05-20 17:26:06 +02:00
committed by GitHub
parent 6c632ddcf3
commit db29c758ef
13 changed files with 657 additions and 14 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -294,14 +294,16 @@ The following code is an example output of the [JSON-ASFF](https://docs.aws.amaz
???+ note
Each finding is a `json` object within a list.
### HTML
The following image is an example of the HTML output:
<img src="../img/reporting/html-output.png">
## V4 Deprecations
Some deprecations have been made to unify formats and improve outputs.
### HTML
HTML output format has been deprecated in favor of the new dashboard, use it with `prowler dashboard`. You can read more about it at [here](dashboard.md).
### JSON

View File

@@ -37,6 +37,7 @@ from prowler.lib.check.custom_checks_metadata import (
from prowler.lib.cli.parser import ProwlerArgumentParser
from prowler.lib.logger import logger, set_logging_config
from prowler.lib.outputs.compliance.compliance import display_compliance_table
from prowler.lib.outputs.html.html import add_html_footer, fill_html_overview_statistics
from prowler.lib.outputs.json.json import close_json
from prowler.lib.outputs.outputs import extract_findings_statistics
from prowler.lib.outputs.slack import send_slack_message
@@ -267,10 +268,21 @@ def prowler():
if "json" in mode:
close_json(
global_provider.output_options.output_filename,
args.output_directory,
global_provider.output_options.output_directory,
mode,
)
if "html" in mode:
add_html_footer(
global_provider.output_options.output_filename,
global_provider.output_options.output_directory,
)
fill_html_overview_statistics(
stats,
global_provider.output_options.output_filename,
global_provider.output_options.output_directory,
)
# Send output to S3 if needed (-B / -D)
if provider == "aws" and (
args.output_bucket or args.output_bucket_no_assume

View File

@@ -12,7 +12,8 @@ from prowler.lib.logger import logger
timestamp = datetime.today()
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
prowler_version = "4.1.0"
square_logo_img = "https://user-images.githubusercontent.com/38561120/235905862-9ece5bd7-9aa3-4e48-807a-3a9035eb8bfb.png"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/master/docs/img/prowler-logo-black.png#gh-light-mode-onlyg"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
azure_logo = "https://user-images.githubusercontent.com/38561120/235927375-b23e2e0f-8932-49ec-b59c-d89f61c8041d.png"
gcp_logo = "https://user-images.githubusercontent.com/38561120/235928332-eb4accdc-c226-4391-8e97-6ca86a91cf50.png"
@@ -57,6 +58,7 @@ csv_file_suffix = ".csv"
json_file_suffix = ".json"
json_asff_file_suffix = ".asff.json"
json_ocsf_file_suffix = ".ocsf.json"
html_file_suffix = ".html"
default_config_file_path = (
f"{pathlib.Path(os.path.dirname(os.path.realpath(__file__)))}/config.yaml"
)

View File

@@ -146,8 +146,8 @@ Detailed documentation at https://docs.prowler.com
"-M",
nargs="+",
help="Output modes, by default csv and json-oscf are saved. When using AWS Security Hub integration, json-asff output is also saved.",
default=["csv", "json-ocsf"],
choices=["csv", "json-asff", "json-ocsf"],
default=["csv", "json-ocsf", "html"],
choices=["csv", "json-asff", "json-ocsf", "html"],
)
common_outputs_parser.add_argument(
"--output-filename",

View File

@@ -4,6 +4,7 @@ from typing import Any
from prowler.config.config import (
csv_file_suffix,
html_file_suffix,
json_asff_file_suffix,
json_ocsf_file_suffix,
)
@@ -25,11 +26,15 @@ from prowler.lib.outputs.compliance.models import (
Check_Output_CSV_KUBERNETES_CIS,
)
from prowler.lib.outputs.csv.csv import generate_csv_fields
from prowler.lib.outputs.html.html import add_html_header
from prowler.lib.utils.utils import file_exists, open_file
def initialize_file_descriptor(
filename: str, output_mode: str, format: Any = FindingOutput
filename: str,
output_mode: str,
provider: Any = None,
format: Any = FindingOutput,
) -> TextIOWrapper:
"""Open/Create the output file. If needed include headers or the required format, by default will use the FindingOutput"""
try:
@@ -46,6 +51,8 @@ def initialize_file_descriptor(
if output_mode in ("json-asff", "json-ocsf"):
file_descriptor.write("[")
elif "html" in output_mode:
add_html_header(file_descriptor, provider)
else:
# Format is the class model of the CSV format to print the headers
csv_header = [x.upper() for x in generate_csv_fields(format)]
@@ -75,6 +82,13 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, provi
)
file_descriptors.update({output_mode: file_descriptor})
elif output_mode == "html":
filename = f"{output_directory}/{output_filename}{html_file_suffix}"
file_descriptor = initialize_file_descriptor(
filename, output_mode, provider
)
file_descriptors.update({output_mode: file_descriptor})
elif output_mode == "json-ocsf":
filename = (
f"{output_directory}/{output_filename}{json_ocsf_file_suffix}"

View File

@@ -0,0 +1,522 @@
import html
import importlib
import sys
from os import path
from prowler.config.config import (
html_file_suffix,
html_logo_url,
prowler_version,
square_logo_img,
timestamp,
)
from prowler.lib.logger import logger
from prowler.lib.outputs.utils import parse_html_string, unroll_dict
from prowler.lib.utils.utils import open_file
def add_html_header(file_descriptor, provider):
try:
file_descriptor.write(
f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- Required meta tags -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<style>
.read-more {{color: #00f;}}
.bg-success-custom {{background-color: #98dea7 !important;}}
.bg-danger {{background-color: #f28484 !important;}}
</style>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous" />
<!-- https://datatables.net/download/index with jQuery, DataTables, Buttons, SearchPanes, and Select //-->
<link rel="stylesheet" type="text/css"
href="https://cdn.datatables.net/v/dt/jqc-1.12.4/dt-1.10.25/b-1.7.1/sp-1.4.0/sl-1.3.3/datatables.min.css" />
<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous" />
<style>
.show-read-more .more-text {{display: none;}}
.dataTable {{font-size: 14px;}}
.container-fluid {{font-size: 14px;}}
.float-left {{ float: left !important; max-width: 100%; }}
</style>
<title>Prowler - The Handy Cloud Security Tool</title>
</head>
<body>
<div class="container-fluid">
<div class="row mt-3">
<div class="col-md-4">
<a href="{html_logo_url}"><img class="float-left card-img-left mt-4 mr-4 ml-4"
src={square_logo_img}
alt="prowler-logo" /></a>
<div class="card">
<div class="card-header">
Report Information
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<div class="row">
<div class="col-md-auto">
<b>Version:</b> {prowler_version}
</div>
</div>
</li>
<li class="list-group-item">
<b>Parameters used:</b> {" ".join(sys.argv[1:])}
</li>
<li class="list-group-item">
<b>Date:</b> {timestamp.isoformat()}
</li>
</ul>
</div>
</div> {get_assessment_summary(provider)}
<div class="col-md-2">
<div class="card">
<div class="card-header">
Assessment Overview
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Total Findings:</b> TOTAL_FINDINGS
</li>
<li class="list-group-item">
<b>Passed:</b> TOTAL_PASS
</li>
<li class="list-group-item">
<b>Failed:</b> TOTAL_FAIL
</li>
<li class="list-group-item">
<b>Total Resources:</b> TOTAL_RESOURCES
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row-mt-3">
<div class="col-md-12">
<table class="table compact stripe row-border ordering" id="findingsTable" data-order='[[ 5, "asc" ]]' data-page-length='100'>
<thead class="thead-light">
<tr>
<th scope="col">Status</th>
<th scope="col">Severity</th>
<th scope="col">Service Name</th>
<th scope="col">Region</th>
<th style="width:20%" scope="col">Check ID</th>
<th style="width:20%" scope="col">Check Title</th>
<th scope="col">Resource ID</th>
<th scope="col">Resource Tags</th>
<th scope="col">Status Extended</th>
<th scope="col">Risk</th>
<th scope="col">Recomendation</th>
<th scope="col">Compliance</th>
</tr>
</thead>
<tbody>
"""
)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def fill_html(file_descriptor, finding):
try:
row_class = "p-3 mb-2 bg-success-custom"
finding.status = finding.status.split(".")[0]
if finding.status == "INFO":
row_class = "table-info"
elif finding.status == "FAIL":
row_class = "table-danger"
elif finding.status == "WARNING":
row_class = "table-warning"
file_descriptor.write(
f"""
<tr class="{row_class}">
<td>{finding.status}</td>
<td>{finding.severity.split(".")[0]}</td>
<td>{finding.service_name}</td>
<td>{finding.region.lower()}</td>
<td>{finding.check_id.replace("_", "<wbr />_")}</td>
<td>{finding.check_title}</td>
<td>{finding.resource_uid.replace("<", "&lt;").replace(">", "&gt;").replace("_", "<wbr />_")}</td>
<td>{parse_html_string(finding.resource_tags)}</td>
<td>{finding.status_extended.replace("<", "&lt;").replace(">", "&gt;").replace("_", "<wbr />_")}</td>
<td><p class="show-read-more">{html.escape(finding.risk)}</p></td>
<td><p class="show-read-more">{html.escape(finding.remediation_recommendation_text)}</p> <a class="read-more" href="{finding.remediation_recommendation_url}"><i class="fas fa-external-link-alt"></i></a></td>
<td><p class="show-read-more">{parse_html_string(unroll_dict(finding.compliance))}</p></td>
</tr>
"""
)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def fill_html_overview_statistics(stats, output_filename, output_directory):
try:
filename = f"{output_directory}/{output_filename}{html_file_suffix}"
# Read file
if path.isfile(filename):
with open(filename, "r") as file:
filedata = file.read()
# Replace statistics
# TOTAL_FINDINGS
filedata = filedata.replace(
"TOTAL_FINDINGS", str(stats.get("findings_count"))
)
# TOTAL_RESOURCES
filedata = filedata.replace(
"TOTAL_RESOURCES", str(stats.get("resources_count"))
)
# TOTAL_PASS
filedata = filedata.replace("TOTAL_PASS", str(stats.get("total_pass")))
# TOTAL_FAIL
filedata = filedata.replace("TOTAL_FAIL", str(stats.get("total_fail")))
# Write file
with open(filename, "w") as file:
file.write(filedata)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def add_html_footer(output_filename, output_directory):
try:
filename = f"{output_directory}/{output_filename}{html_file_suffix}"
# Close HTML file if exists
if path.isfile(filename):
file_descriptor = open_file(
filename,
"a",
)
file_descriptor.write(
"""
</tbody>
</table>
</div>
</div>
<!-- Table search and paginator -->
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"
integrity="sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd"
crossorigin="anonymous"></script>
<!-- https://datatables.net/download/index with jQuery, DataTables, Buttons, SearchPanes, and Select //-->
<script type="text/javascript"
src="https://cdn.datatables.net/v/dt/jqc-1.12.4/dt-1.10.25/b-1.7.1/sp-1.4.0/sl-1.3.3/datatables.min.js"></script>
<script>
$(document).ready(function () {
// Initialise the table with 50 rows, and some search/filtering panes
$('#findingsTable').DataTable({
responsive: true,
// Show 25, 50, 100 and All records
lengthChange: true,
lengthMenu: [[25, 50, 100, -1], [25, 50, 100, "All"]],
searchPanes: {
cascadePanes: true,
viewTotal: true,
},
dom: 'Blfrtip',
language: {
// To enable a filter button instead of the filter row
searchPanes: {
clearMessage: 'Clear Filters',
collapse: { 0: 'Filters', _: 'Filters (%d)' },
initCollapsed: true
}
},
buttons: [
{
extend: 'searchPanes',
config: {
cascadePanes: true,
viewTotal: true,
orderable: false
}
}
],
columnDefs: [
{
searchPanes: {
show: true,
pagingType: 'numbers',
searching: true
},
// Show all filters
targets: [0, 1, 2, 3, 5, 7]
}
]
});
var maxLength = 30;
// ReadMore ReadLess
$(".show-read-more").each(function () {
var myStr = $(this).text();
if ($.trim(myStr).length > maxLength) {
var newStr = myStr.substring(0, maxLength);
var removedStr = myStr.substring(maxLength, $.trim(myStr).length);
$(this).empty().html(newStr);
$(this).append(' <a href="javascript:void(0);" class="read-more">read more...</a>');
$(this).append('<span class="more-text">' + removedStr + '</span>');
}
});
$(".read-more").click(function () {
$(this).siblings(".more-text").contents().unwrap();
$(this).remove();
});
});
</script>
</body>
</html>
"""
)
file_descriptor.close()
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def get_aws_html_assessment_summary(provider):
try:
if provider.__class__.__name__ == "AwsProvider":
profile = (
provider._identity.profile
if provider._identity.profile is not None
else "default"
)
if isinstance(provider._identity.audited_regions, list):
audited_regions = " ".join(provider._identity.audited_regions)
elif not provider._identity.audited_regions:
audited_regions = "All Regions"
else:
audited_regions = ", ".join(provider._identity.audited_regions)
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
AWS Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>AWS Account:</b> {provider._identity.account}
</li>
<li class="list-group-item">
<b>AWS-CLI Profile:</b> {profile}
</li>
<li class="list-group-item">
<b>Audited Regions:</b> {audited_regions}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
AWS Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>User Id:</b> {provider._identity.user_id}
</li>
<li class="list-group-item">
<b>Caller Identity ARN:</b> {provider._identity.identity_arn}
</li>
</ul>
</div>
</div>
"""
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def get_azure_html_assessment_summary(provider):
try:
if provider.__class__.__name__ == "AzureProvider":
printed_subscriptions = []
for key, value in provider._identity.subscriptions.items():
intermediate = f"{key} : {value}"
printed_subscriptions.append(intermediate)
# check if identity is str(coming from SP) or dict(coming from browser or)
if isinstance(provider._identity.identity_id, dict):
html_identity = provider._identity.identity_id.get(
"userPrincipalName", "Identity not found"
)
else:
html_identity = provider._identity.identity_id
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Azure Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Azure Tenant IDs:</b> {" ".join(provider._identity.tenant_ids)}
</li>
<li class="list-group-item">
<b>Azure Tenant Domain:</b> {provider._identity.tenant_domain}
</li>
<li class="list-group-item">
<b>Azure Subscriptions:</b> {" ".join(printed_subscriptions)}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Azure Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Azure Identity Type:</b> {provider._identity.identity_type}
</li>
<li class="list-group-item">
<b>Azure Identity ID:</b> {html_identity}
</li>
</ul>
</div>
</div>
"""
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def get_gcp_html_assessment_summary(provider):
try:
if provider.__class__.__name__ == "GcpProvider":
try:
getattr(provider.credentials, "_service_account_email")
profile = (
provider.credentials._service_account_email
if provider.credentials._service_account_email is not None
else "default"
)
except AttributeError:
profile = "default"
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
GCP Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>GCP Project IDs:</b> {", ".join(provider.project_ids)}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
GCP Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>GCP Account:</b> {profile}
</li>
</ul>
</div>
</div>
"""
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def get_kubernetes_html_assessment_summary(provider):
try:
if provider.__class__.__name__ == "KubernetesProvider":
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Kubernetes Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>Kubernetes Cluster:</b> {provider._identity.cluster}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Kubernetes Credentials
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>Kubernetes Context:</b> {provider._identity.context}
</li>
</ul>
</div>
</div>
"""
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
sys.exit(1)
def get_assessment_summary(provider):
"""
get_assessment_summary gets the HTML assessment summary for the provider
"""
try:
# This is based in the Provider_provider class
# It is not pretty but useful
# AWS_provider --> aws
# GCP_provider --> gcp
# Azure_provider --> azure
# Dynamically get the Provider quick inventory handler
provider_html_assessment_summary_function = (
f"get_{provider.type}_html_assessment_summary"
)
return getattr(
importlib.import_module(__name__), provider_html_assessment_summary_function
)(provider)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
sys.exit(1)

View File

@@ -18,6 +18,7 @@ from prowler.lib.outputs.compliance.compliance import (
)
from prowler.lib.outputs.csv.csv import generate_csv_fields
from prowler.lib.outputs.file_descriptors import fill_file_descriptors
from prowler.lib.outputs.html.html import fill_html
from prowler.lib.outputs.json_asff.json_asff import fill_json_asff
from prowler.lib.outputs.json_ocsf.json_ocsf import fill_json_ocsf
from prowler.lib.outputs.utils import unroll_dict, unroll_list
@@ -139,6 +140,9 @@ def report(check_findings, provider):
)
file_descriptors["json-ocsf"].write(",")
if "html" in file_descriptors:
fill_html(file_descriptors["html"], finding_output)
# CSV
if "csv" in file_descriptors:
finding_output.compliance = unroll_dict(

View File

@@ -5,6 +5,7 @@ from tabulate import tabulate
from prowler.config.config import (
csv_file_suffix,
html_file_suffix,
json_asff_file_suffix,
json_ocsf_file_suffix,
orange_color,
@@ -133,6 +134,10 @@ def display_summary_table(
)
if "csv" in output_options.output_modes:
print(f" - CSV: {output_directory}/{output_filename}{csv_file_suffix}")
if "html" in output_options.output_modes:
print(
f" - HTML: {output_directory}/{output_filename}{html_file_suffix}"
)
else:
print(

View File

@@ -86,3 +86,12 @@ def parse_json_tags(tags: list):
dict_tags.update(tag)
return dict_tags
def parse_html_string(str: str):
string = ""
for elem in str.split(" | "):
if elem:
string += f"\n&#x2022;{elem}\n"
return string

View File

@@ -44,9 +44,10 @@ class Test_Parser:
parsed = self.parser.parse(command)
assert parsed.provider == provider
assert not parsed.status
assert len(parsed.output_formats) == 2
assert len(parsed.output_formats) == 3
assert "csv" in parsed.output_formats
assert "json-ocsf" in parsed.output_formats
assert "html" in parsed.output_formats
assert not parsed.output_filename
assert "output" in parsed.output_directory
assert not parsed.verbose
@@ -90,9 +91,10 @@ class Test_Parser:
parsed = self.parser.parse(command)
assert parsed.provider == provider
assert not parsed.status
assert len(parsed.output_formats) == 2
assert len(parsed.output_formats) == 3
assert "csv" in parsed.output_formats
assert "json-ocsf" in parsed.output_formats
assert "html" in parsed.output_formats
assert not parsed.output_filename
assert "output" in parsed.output_directory
assert not parsed.verbose
@@ -129,9 +131,10 @@ class Test_Parser:
parsed = self.parser.parse(command)
assert parsed.provider == provider
assert not parsed.status
assert len(parsed.output_formats) == 2
assert len(parsed.output_formats) == 3
assert "csv" in parsed.output_formats
assert "json-ocsf" in parsed.output_formats
assert "html" in parsed.output_formats
assert not parsed.output_filename
assert "output" in parsed.output_directory
assert not parsed.verbose
@@ -163,9 +166,10 @@ class Test_Parser:
parsed = self.parser.parse(command)
assert parsed.provider == provider
assert not parsed.severity
assert len(parsed.output_formats) == 2
assert len(parsed.output_formats) == 3
assert "csv" in parsed.output_formats
assert "json-ocsf" in parsed.output_formats
assert "html" in parsed.output_formats
assert not parsed.output_filename
assert "output" in parsed.output_directory
assert not parsed.verbose
@@ -264,9 +268,10 @@ class Test_Parser:
def test_root_parser_default_output_formats(self):
command = [prowler_command]
parsed = self.parser.parse(command)
assert len(parsed.output_formats) == 2
assert len(parsed.output_formats) == 3
assert "csv" in parsed.output_formats
assert "json-ocsf" in parsed.output_formats
assert "html" in parsed.output_formats
def test_root_parser_output_formats_short(self):
command = [prowler_command, "-M", "csv"]
@@ -292,6 +297,18 @@ class Test_Parser:
assert len(parsed.output_formats) == 1
assert "json-ocsf" in parsed.output_formats
def test_root_parser_output_formats_short_html(self):
command = [prowler_command, "-M", "html"]
parsed = self.parser.parse(command)
assert len(parsed.output_formats) == 1
assert "html" in parsed.output_formats
def test_root_parser_output_formats_long_html(self):
command = [prowler_command, "--output-modes", "html"]
parsed = self.parser.parse(command)
assert len(parsed.output_formats) == 1
assert "html" in parsed.output_formats
def test_root_parser_output_filename_short(self):
filename = "test_output.txt"
command = [prowler_command, "-F", filename]

View File

@@ -7,6 +7,7 @@ from colorama import Fore
from prowler.config.config import (
csv_file_suffix,
html_file_suffix,
json_asff_file_suffix,
json_ocsf_file_suffix,
output_file_timestamp,
@@ -23,6 +24,7 @@ from prowler.lib.outputs.csv.csv import generate_csv_fields
from prowler.lib.outputs.file_descriptors import fill_file_descriptors
from prowler.lib.outputs.outputs import extract_findings_statistics, set_report_color
from prowler.lib.outputs.utils import (
parse_html_string,
parse_json_tags,
unroll_dict,
unroll_dict_to_list,
@@ -42,7 +44,8 @@ class TestOutputs:
["csv"],
["json-asff"],
["json-ocsf"],
["csv", "json-asff", "json-ocsf"],
["html"],
["csv", "json-asff", "json-ocsf", "html"],
]
output_filename = f"prowler-output-{audited_account}-{output_file_timestamp}"
expected = [
@@ -64,6 +67,12 @@ class TestOutputs:
"a",
)
},
{
"html": open_file(
f"{output_directory}/{output_filename}{html_file_suffix}",
"a",
)
},
{
"csv": open_file(
f"{output_directory}/{output_filename}{csv_file_suffix}",
@@ -77,6 +86,10 @@ class TestOutputs:
f"{output_directory}/{output_filename}{json_ocsf_file_suffix}",
"a",
),
"html": open_file(
f"{output_directory}/{output_filename}{html_file_suffix}",
"a",
),
},
]
@@ -167,6 +180,49 @@ class TestOutputs:
assert unroll_list(list, ",") == "test, test1, test2"
def test_parse_html_string(self):
string = "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13"
assert (
parse_html_string(string)
== """
&#x2022;CISA: your-systems-3, your-data-1, your-data-2
&#x2022;CIS-1.4: 2.1.1
&#x2022;CIS-1.5: 2.1.1
&#x2022;GDPR: article_32
&#x2022;AWS-Foundational-Security-Best-Practices: s3
&#x2022;HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii
&#x2022;GxP-21-CFR-Part-11: 11.10-c, 11.30
&#x2022;GxP-EU-Annex-11: 7.1-data-storage-damage-protection
&#x2022;NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16
&#x2022;NIST-800-53-Revision-4: sc_28
&#x2022;NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4
&#x2022;ENS-RD2022: mp.si.2.aws.s3.1
&#x2022;NIST-CSF-1.1: ds_1
&#x2022;RBI-Cyber-Security-Framework: annex_i_1_3
&#x2022;FFIEC: d3-pc-am-b-12
&#x2022;PCI-3.2.1: s3
&#x2022;FedRamp-Moderate-Revision-4: sc-13, sc-28
&#x2022;FedRAMP-Low-Revision-4: sc-13
"""
)
def test_unroll_tags(self):
dict_list = [
{"Key": "name", "Value": "test"},