Files
prowler/prowler/lib/outputs/html/html.py
T

1901 lines
76 KiB
Python

import re
import sys
from io import TextIOWrapper
from urllib.parse import urlparse
import markdown
from markupsafe import escape
from prowler.config.config import (
html_logo_url,
prowler_version,
square_logo_img,
timestamp,
)
from prowler.lib.cli.redact import redact_argv
from prowler.lib.logger import logger
from prowler.lib.outputs.output import Finding, Output
from prowler.lib.outputs.utils import parse_html_string, unroll_dict
from prowler.providers.common.provider import Provider
_SAFE_URL_SCHEMES = {"http", "https"}
def _safe_url(url: str) -> str:
"""Return url if its scheme is http/https, otherwise return empty string."""
if not url:
return ""
scheme = urlparse(url).scheme.lower()
return url if scheme in _SAFE_URL_SCHEMES else ""
def _strip_unsafe_links(html_content: str) -> str:
"""Replace <a href> tags whose href is not http/https with their link text."""
def _replace(match: re.Match) -> str:
href = match.group("href")
body = match.group("body")
safe = _safe_url(href)
return f'<a href="{safe}">{body}</a>' if safe else body
return re.sub(
r'<a\s[^>]*href="(?P<href>[^"]*)"[^>]*>(?P<body>.*?)</a>',
_replace,
html_content,
flags=re.IGNORECASE | re.DOTALL,
)
class HTML(Output):
@staticmethod
def process_markdown(text: str) -> str:
"""
Process markdown syntax in text and convert to HTML using the markdown library.
Args:
text (str): Text containing markdown syntax
Returns:
str: HTML with markdown syntax converted
"""
if not text:
return text
# Initialize markdown converter with safe mode to prevent XSS
md = markdown.Markdown(extensions=["nl2br"])
# Convert markdown to HTML
html_content = md.convert(text)
# Strip outer <p> tags if present, as we're embedding in existing HTML
# Handle single paragraph case
if (
html_content.startswith("<p>")
and html_content.endswith("</p>")
and html_content.count("<p>") == 1
):
html_content = html_content[3:-4]
# Handle multiple paragraphs case - replace <p> and </p> with <br><br>
elif "<p>" in html_content and "</p>" in html_content:
html_content = html_content.replace("</p>\n<p>", "<br />\n<br />\n")
html_content = html_content.replace("<p>", "")
html_content = html_content.replace("</p>", "")
return _strip_unsafe_links(html_content)
def transform(self, findings: list[Finding]) -> None:
"""Transforms the findings into the HTML format.
Args:
findings (list[Finding]): a list of Finding objects
"""
try:
for finding in findings:
row_class = "p-3 mb-2 bg-success-custom"
finding_status = finding.status.value
# Change the status of the finding if it's muted
if finding.muted:
finding_status = f"MUTED ({finding_status})"
row_class = "table-warning"
if finding.status == "MANUAL":
row_class = "table-info"
elif finding.status == "FAIL":
row_class = "table-danger"
self._data.append(f"""
<tr class="{row_class}">
<td>{finding_status}</td>
<td>{str(escape(finding.metadata.Severity.value))}</td>
<td>{str(escape(finding.metadata.ServiceName))}</td>
<td>{str(escape(finding.region.lower()))}</td>
<td>{str(escape(finding.metadata.CheckID)).replace("_", "<wbr />_")}</td>
<td>{str(escape(finding.metadata.CheckTitle))}</td>
<td>{str(escape(finding.resource_uid)).replace("_", "<wbr />_")}</td>
<td>{parse_html_string(str(escape(unroll_dict(finding.resource_tags))))}</td>
<td>{str(escape(finding.status_extended)).replace("_", "<wbr />_")}</td>
<td><p class="show-read-more">{HTML.process_markdown(str(escape(finding.metadata.Risk)))}</p></td>
<td><p class="show-read-more">{HTML.process_markdown(str(escape(finding.metadata.Remediation.Recommendation.Text)))}</p> <a class="read-more" href="{str(escape(_safe_url(finding.metadata.Remediation.Recommendation.Url)))}"><i class="fas fa-external-link-alt"></i></a></td>
<td><p class="show-read-more">{parse_html_string(str(escape(unroll_dict(finding.compliance, separator=": "))))}</p></td>
</tr>
""")
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def batch_write_data_to_file(self, provider: Provider, stats: dict) -> None:
"""
Writes the findings to a file using the HTML format using the `Output._file_descriptor`.
Args:
provider (Provider): the provider object
output_filename (str): the name of the output file
output_directory (str): the directory where the output file will be saved
stats (dict): the statistics of the findings
"""
try:
if (
getattr(self, "_file_descriptor", None)
and not self._file_descriptor.closed
and self._data
):
if self._file_descriptor.tell() == 0:
HTML.write_header(
self._file_descriptor, provider, stats, self._from_cli
)
for finding in self._data:
self._file_descriptor.write(finding)
if self.close_file or self._from_cli:
HTML.write_footer(self._file_descriptor)
self._file_descriptor.close()
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@staticmethod
def write_header(
file_descriptor: TextIOWrapper,
provider: Provider,
stats: dict,
from_cli: bool = True,
) -> None:
"""
Writes the header of the HTML file.
Args:
file_descriptor (file): the file descriptor to write the header
provider (Provider): the provider object
stats (dict): the statistics of the findings
from_cli (bool): whether the request is from the CLI or not
"""
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"
style="width: 15rem; height:auto;"/></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> {redact_argv(sys.argv[1:]) if from_cli else ""}
</li>
<li class="list-group-item">
<b>Date:</b> {timestamp.isoformat()}
</li>
</ul>
</div>
</div>{HTML.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> {str(stats.get("findings_count", 0))}
</li>
<li class="list-group-item">
<b>Passed:</b> {str(stats.get("total_pass", 0))}
</li>
<li class="list-group-item">
<b>Passed (Muted):</b> {str(stats.get("total_muted_pass", 0))}
</li>
<li class="list-group-item">
<b>Failed:</b> {str(stats.get("total_fail", 0))}
</li>
<li class="list-group-item">
<b>Failed (Muted):</b> {str(stats.get("total_muted_fail", 0))}
</li>
<li class="list-group-item">
<b>Total Resources:</b> {str(stats.get("resources_count", 0))}
</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">Recommendation</th>
<th scope="col">Compliance</th>
</tr>
</thead>
<tbody>""")
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
@staticmethod
def write_footer(file_descriptor: TextIOWrapper) -> None:
"""
Writes the footer of the HTML file.
Args:
file_descriptor (file): the file descriptor to write the footer
"""
try:
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).html();
var textLength = $(this).text().length;
if (textLength > maxLength) {
// Find the position where to cut while preserving HTML tags and breaking at word boundaries
var cutPosition = 0;
var currentLength = 0;
var inTag = false;
var lastWordBoundary = 0;
var tagStack = [];
for (var i = 0; i < myStr.length; i++) {
if (myStr[i] === '<') {
inTag = true;
// Track opening tags
if (myStr[i + 1] !== '/') {
var tagEnd = myStr.indexOf('>', i);
if (tagEnd !== -1) {
var tagName = myStr.substring(i + 1, tagEnd).split(' ')[0];
tagStack.push(tagName);
}
} else {
// Closing tag
var tagEnd = myStr.indexOf('>', i);
if (tagEnd !== -1) {
var tagName = myStr.substring(i + 2, tagEnd).split(' ')[0];
if (tagStack.length > 0) {
tagStack.pop();
}
}
}
} else if (myStr[i] === '>') {
inTag = false;
} else if (!inTag) {
currentLength++;
// Only consider word boundaries if we're not inside any HTML tags
if (tagStack.length === 0 && (myStr[i] === ' ' || myStr[i] === '.' || myStr[i] === ',' || myStr[i] === ';' || myStr[i] === ':' || myStr[i] === '!' || myStr[i] === '?')) {
lastWordBoundary = i + 1;
}
if (currentLength >= maxLength) {
// If we're inside HTML tags, find the next closing tag
if (tagStack.length > 0) {
// Find the next closing tag for the current open tag
var nextClosingTag = '</' + tagStack[tagStack.length - 1] + '>';
var closingTagPos = myStr.indexOf(nextClosingTag, i);
if (closingTagPos !== -1) {
cutPosition = closingTagPos + nextClosingTag.length;
} else {
// If no closing tag found, use current position
cutPosition = i + 1;
}
} else {
// Use the last word boundary if available, otherwise use current position
cutPosition = lastWordBoundary > 0 ? lastWordBoundary : i + 1;
}
break;
}
}
}
var newStr = myStr.substring(0, cutPosition);
var removedStr = myStr.substring(cutPosition);
$(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>
""")
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
@staticmethod
def get_aws_assessment_summary(provider: Provider) -> str:
"""
get_aws_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
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)
account = escape(str(provider.identity.account))
profile = escape(str(profile))
audited_regions = escape(str(audited_regions))
user_id = escape(str(provider.identity.user_id))
identity_arn = escape(str(provider.identity.identity_arn))
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> {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> {user_id}
</li>
<li class="list-group-item">
<b>Caller Identity ARN:</b> {identity_arn}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_azure_assessment_summary(provider: Provider) -> str:
"""
get_azure_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
printed_subscriptions = []
for (
subscription_id,
display_name,
) in provider.identity.subscriptions.items():
intermediate = f"{display_name} : {subscription_id}"
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
tenant_ids = escape(" ".join(provider.identity.tenant_ids))
tenant_domain = escape(str(provider.identity.tenant_domain))
subscriptions = escape(" ".join(printed_subscriptions))
identity_type = escape(str(provider.identity.identity_type))
html_identity = escape(str(html_identity))
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> {tenant_ids}
</li>
<li class="list-group-item">
<b>Azure Tenant Domain:</b> {tenant_domain}
</li>
<li class="list-group-item">
<b>Azure Subscriptions:</b> {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> {identity_type}
</li>
<li class="list-group-item">
<b>Azure Identity ID:</b> {html_identity}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_gcp_assessment_summary(provider: Provider) -> str:
"""
get_gcp_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
try:
getattr(provider.session, "_service_account_email")
profile = (
provider.session._service_account_email
if provider.session._service_account_email is not None
else "default"
)
except AttributeError:
profile = "default"
project_ids = escape(", ".join(provider.project_ids))
profile = escape(str(profile))
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> {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.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_kubernetes_assessment_summary(provider: Provider) -> str:
"""
get_kubernetes_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
cluster = escape(str(provider.identity.cluster))
context = escape(str(provider.identity.context))
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> {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> {context}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_github_assessment_summary(provider: Provider) -> str:
"""
get_github_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
auth_method = escape(str(provider.auth_method))
if hasattr(provider.identity, "account_name"):
# GithubIdentityInfo (Personal Access Token, OAuth)
account_name = escape(str(provider.identity.account_name))
account_info_items = f"""
<li class="list-group-item">
<b>GitHub account:</b> {account_name}
</li>
"""
# Add email if available
if (
hasattr(provider.identity, "account_email")
and provider.identity.account_email
):
account_email = escape(str(provider.identity.account_email))
account_info_items += f"""
<li class="list-group-item">
<b>GitHub account email:</b> {account_email}
</li>"""
elif hasattr(provider.identity, "app_id"):
# GithubAppIdentityInfo (GitHub App)
# Assessment items: App Name and Installations
app_name = escape(str(provider.identity.app_name))
account_info_items = f"""
<li class="list-group-item">
<b>GitHub App Name:</b> {app_name}
</li>"""
# Add installations if available
if (
hasattr(provider.identity, "installations")
and provider.identity.installations
):
installations_display = escape(
", ".join(provider.identity.installations)
)
account_info_items += f"""
<li class="list-group-item">
<b>Installations:</b> {installations_display}
</li>"""
else:
account_info_items += """
<li class="list-group-item">
<b>Installations:</b> No installations found
</li>"""
# Credentials items: Authentication method and App ID
app_id = escape(str(provider.identity.app_id))
credentials_items = f"""
<li class="list-group-item">
<b>GitHub authentication method:</b> {auth_method}
</li>
<li class="list-group-item">
<b>GitHub App ID:</b> {app_id}
</li>"""
else:
# Fallback for other identity types
account_info_items = ""
credentials_items = f"""
<li class="list-group-item">
<b>GitHub authentication method:</b> {auth_method}
</li>"""
# For PAT/OAuth, use default credentials structure
if hasattr(provider.identity, "account_name"):
credentials_items = f"""
<li class="list-group-item">
<b>GitHub authentication method:</b> {auth_method}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
GitHub Assessment Summary
</div>
<ul class="list-group list-group-flush">
{account_info_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
GitHub Credentials
</div>
<ul class="list-group list-group-flush">
{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_m365_assessment_summary(provider: Provider) -> str:
"""
get_m365_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
tenant_domain = escape(str(provider.identity.tenant_domain))
identity_type = escape(str(provider.identity.identity_type))
identity_id = escape(str(provider.identity.identity_id))
user_item = ""
if (
hasattr(provider.identity, "user")
and provider.identity.user is not None
):
user = escape(str(provider.identity.user))
user_item = f"""<li class="list-group-item">
<b>M365 User:</b> {user}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
M365 Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>M365 Tenant Domain:</b> {tenant_domain}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
M365 Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>M365 Identity Type:</b> {identity_type}
</li>
<li class="list-group-item">
<b>M365 Identity ID:</b> {identity_id}
</li>
{user_item}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
def get_nhn_assessment_summary(provider: Provider) -> str:
"""
get_nhn_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
tenant_domain = escape(str(provider.identity.tenant_domain))
identity_type = escape(str(provider.identity.identity_type))
identity_id = escape(str(provider.identity.identity_id))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
NHN Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>NHN Tenant Domain:</b> {tenant_domain}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
NHN Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>NHN Identity Type:</b> {identity_type}
</li>
<li class="list-group-item">
<b>NHN Identity ID:</b> {identity_id}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_mongodbatlas_assessment_summary(provider: Provider) -> str:
"""
get_mongodbatlas_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
organization_name = escape(str(provider.identity.organization_name))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
MongoDB Atlas Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>MongoDB Atlas organization:</b> {organization_name}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
MongoDB Atlas Credentials
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>MongoDB Atlas authentication method:</b> API Key
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_iac_assessment_summary(provider: Provider) -> str:
"""
get_iac_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
if provider.scan_repository_url:
target_info = "<b>IAC repository URL:</b> " + str(
escape(str(provider.scan_repository_url))
)
else:
target_info = "<b>IAC path:</b> " + str(escape(str(provider.scan_path)))
auth_method = escape(str(provider.auth_method))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
IAC Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
{target_info}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
IAC Credentials
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>IAC authentication method:</b> {auth_method}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_image_assessment_summary(provider: Provider) -> str:
"""
get_image_assessment_summary gets the HTML assessment summary for the Image provider
Args:
provider (Provider): the Image provider object
Returns:
str: the HTML assessment summary
"""
try:
if provider.registry:
registry = escape(str(provider.registry))
target_info = f"<b>Registry URL:</b> {registry}"
else:
images = escape(", ".join(provider.images))
target_info = f"<b>Images:</b> {images}"
auth_method = escape(str(provider.auth_method))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Image Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
{target_info}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Image Credentials
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>Image authentication method:</b> {auth_method}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_llm_assessment_summary(provider: Provider) -> str:
"""
get_llm_assessment_summary gets the HTML assessment summary for the LLM provider
Args:
provider (Provider): the LLM provider object
Returns:
str: HTML assessment summary for the LLM provider
"""
try:
model = escape(str(provider.model))
plugins = escape(", ".join(provider.plugins))
max_concurrency = escape(str(provider.max_concurrency))
config_file = escape(
str(provider.config_path)
if provider.config_path
else "Using promptfoo defaults"
)
return f"""
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-robot"></i> LLM Security Assessment Summary
</h5>
</div>
<div class="card-body">
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>Target LLM:</b> {model}
</li>
<li class="list-group-item">
<b>Plugins:</b> {plugins}
</li>
<li class="list-group-item">
<b>Max concurrency:</b> {max_concurrency}
</li>
<li class="list-group-item">
<b>Config file:</b> {config_file}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_oraclecloud_assessment_summary(provider: Provider) -> str:
"""
get_oraclecloud_assessment_summary gets the HTML assessment summary for the OracleCloud provider
Args:
provider (Provider): the OracleCloud provider object
Returns:
str: HTML assessment summary for the OracleCloud provider
"""
try:
profile = getattr(provider.session, "profile", "default")
if profile is None:
profile = "instance-principal"
tenancy_name = getattr(provider.identity, "tenancy_name", "unknown")
tenancy_id = getattr(provider.identity, "tenancy_id", "unknown")
tenancy = escape(
str(tenancy_name if tenancy_name != "unknown" else tenancy_id)
)
profile = escape(str(profile))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
OracleCloud Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>OracleCloud Tenancy:</b> {tenancy}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
OracleCloud Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Profile:</b> {profile}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_stackit_assessment_summary(provider: Provider) -> str:
"""
get_stackit_assessment_summary gets the HTML assessment summary for the StackIT provider
Args:
provider (Provider): the StackIT provider object
Returns:
str: HTML assessment summary for the StackIT provider
"""
try:
project_id = getattr(provider.identity, "project_id", "unknown")
project_name = getattr(provider.identity, "project_name", "")
audited_regions = getattr(provider.identity, "audited_regions", set())
project_id = escape(str(project_id))
project_name_item = (
f"""
<li class="list-group-item">
<b>Project Name:</b> {escape(str(project_name))}
</li>"""
if project_name
else ""
)
regions_item = (
f"""
<li class="list-group-item">
<b>Regions:</b> {escape(", ".join(sorted(audited_regions)))}
</li>"""
if audited_regions
else ""
)
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
StackIT Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Project ID:</b> {project_id}
</li>
{project_name_item}
{regions_item}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
StackIT Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Authentication Type:</b> Service Account Key
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_cloudflare_assessment_summary(provider: Provider) -> str:
"""
get_cloudflare_assessment_summary gets the HTML assessment summary for the Cloudflare provider
Args:
provider (Provider): the Cloudflare provider object
Returns:
str: HTML assessment summary for the Cloudflare provider
"""
try:
# Build assessment summary items (only non-None values)
assessment_items = ""
if provider.accounts:
accounts = escape(", ".join([str(acc.id) for acc in provider.accounts]))
assessment_items += f"""
<li class="list-group-item">
<b>Accounts:</b> {accounts}
</li>"""
# Build credentials items (only non-None values)
credentials_items = ""
# Authentication method
if provider.session.api_token:
credentials_items += """
<li class="list-group-item">
<b>Authentication:</b> API Token
</li>"""
elif provider.session.api_key and provider.session.api_email:
credentials_items += """
<li class="list-group-item">
<b>Authentication:</b> API Key + Email
</li>"""
# Email (from identity or session)
email = getattr(provider.identity, "email", None) or getattr(
provider.session, "api_email", None
)
if email:
email = escape(str(email))
credentials_items += f"""
<li class="list-group-item">
<b>Email:</b> {email}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Cloudflare Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Cloudflare Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_alibabacloud_assessment_summary(provider: Provider) -> str:
"""
get_alibabacloud_assessment_summary gets the HTML assessment summary for the Alibaba Cloud provider
Args:
provider (Provider): the Alibaba Cloud provider object
Returns:
str: HTML assessment summary for the Alibaba Cloud provider
"""
try:
account_id = getattr(provider.identity, "account_id", "unknown")
account_name = getattr(provider.identity, "account_name", "")
audited_regions = getattr(
provider.identity, "audited_regions", "All Regions"
)
identity_arn = getattr(provider.identity, "identity_arn", "unknown")
user_name = getattr(provider.identity, "user_name", "unknown")
account_name_item = (
f"""
<li class="list-group-item">
<b>Account Name:</b> {escape(str(account_name))}
</li>"""
if account_name
else ""
)
account_id = escape(str(account_id))
audited_regions = escape(str(audited_regions))
user_name = escape(str(user_name))
identity_arn = escape(str(identity_arn))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Alibaba Cloud Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Account ID:</b> {account_id}
</li>
{account_name_item}
<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">
Alibaba Cloud Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>User Name:</b> {user_name}
</li>
<li class="list-group-item">
<b>Identity ARN:</b> {identity_arn}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_openstack_assessment_summary(provider: Provider) -> str:
"""
get_openstack_assessment_summary gets the HTML assessment summary for the OpenStack provider
Args:
provider (Provider): the OpenStack provider object
Returns:
str: HTML assessment summary for the OpenStack provider
"""
try:
project_id = getattr(provider.identity, "project_id", "unknown")
project_name = getattr(provider.identity, "project_name", "")
region_name = getattr(provider.identity, "region_name", "unknown")
username = getattr(provider.identity, "username", "unknown")
user_id = getattr(provider.identity, "user_id", "")
project_name_item = (
f"""
<li class="list-group-item">
<b>Project Name:</b> {escape(str(project_name))}
</li>"""
if project_name
else ""
)
user_id_item = (
f"""
<li class="list-group-item">
<b>User ID:</b> {escape(str(user_id))}
</li>"""
if user_id
else ""
)
project_id = escape(str(project_id))
region_name = escape(str(region_name))
username = escape(str(username))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
OpenStack Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Project ID:</b> {project_id}
</li>
{project_name_item}
<li class="list-group-item">
<b>Region:</b> {region_name}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
OpenStack Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Username:</b> {username}
</li>
{user_id_item}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_googleworkspace_assessment_summary(provider: Provider) -> str:
"""
get_googleworkspace_assessment_summary gets the HTML assessment summary for the Google Workspace provider
Args:
provider (Provider): the Google Workspace provider object
Returns:
str: HTML assessment summary for the Google Workspace provider
"""
try:
domain = escape(str(provider.identity.domain))
customer_id = escape(str(provider.identity.customer_id))
delegated_user = escape(str(provider.identity.delegated_user))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Google Workspace Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Domain:</b> {domain}
</li>
<li class="list-group-item">
<b>Customer ID:</b> {customer_id}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Google Workspace Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Delegated User:</b> {delegated_user}
</li>
<li class="list-group-item">
<b>Authentication Method:</b> Service Account with Domain-Wide Delegation
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_e2enetworks_assessment_summary(provider: Provider) -> str:
"""Get the HTML assessment summary for the E2E Networks provider."""
try:
locations = escape(", ".join(provider.identity.locations))
project_id = escape(str(provider.identity.project_id))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
E2E Networks Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Project ID:</b> {project_id}
</li>
<li class="list-group-item">
<b>Locations:</b> {locations}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
E2E Networks Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Authentication:</b> API Key + Bearer Token
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_vercel_assessment_summary(provider: Provider) -> str:
"""
get_vercel_assessment_summary gets the HTML assessment summary for the Vercel provider
Args:
provider (Provider): the Vercel provider object
Returns:
str: HTML assessment summary for the Vercel provider
"""
try:
assessment_items = ""
team = getattr(provider.identity, "team", None)
if team:
team_name = escape(str(team.name))
team_id = escape(str(team.id))
assessment_items += f"""
<li class="list-group-item">
<b>Team:</b> {team_name} ({team_id})
</li>"""
credentials_items = """
<li class="list-group-item">
<b>Authentication:</b> API Token
</li>"""
email = getattr(provider.identity, "email", None)
if email:
email = escape(str(email))
credentials_items += f"""
<li class="list-group-item">
<b>Email:</b> {email}
</li>"""
username = getattr(provider.identity, "username", None)
if username:
username = escape(str(username))
credentials_items += f"""
<li class="list-group-item">
<b>Username:</b> {username}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Vercel Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Vercel Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_okta_assessment_summary(provider: Provider) -> str:
"""
get_okta_assessment_summary gets the HTML assessment summary for the Okta provider
Args:
provider (Provider): the Okta provider object
Returns:
str: HTML assessment summary for the Okta provider
"""
try:
org_domain = escape(str(provider.identity.org_domain))
auth_method = escape(str(provider.auth_method))
client_id = escape(str(provider.identity.client_id))
assessment_items = f"""
<li class="list-group-item">
<b>Okta Domain:</b> {org_domain}
</li>"""
credentials_items = f"""
<li class="list-group-item">
<b>Authentication:</b> {auth_method}
</li>
<li class="list-group-item">
<b>Client ID:</b> {client_id}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Okta Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Okta Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_scaleway_assessment_summary(provider: Provider) -> str:
"""
get_scaleway_assessment_summary gets the HTML assessment summary for the Scaleway provider
Args:
provider (Provider): the Scaleway provider object
Returns:
str: HTML assessment summary for the Scaleway provider
"""
try:
organization_id = escape(str(provider.identity.organization_id))
assessment_items = f"""
<li class="list-group-item">
<b>Organization ID:</b> {organization_id}
</li>"""
credentials_items = """
<li class="list-group-item">
<b>Authentication:</b> API Key
</li>"""
access_key = getattr(provider.session, "access_key", None)
if access_key:
access_key = escape(str(access_key))
credentials_items += f"""
<li class="list-group-item">
<b>Access Key:</b> {access_key}
</li>"""
bearer_type = getattr(provider.identity, "bearer_type", None)
bearer_email = getattr(provider.identity, "bearer_email", None)
bearer_id = getattr(provider.identity, "bearer_id", None)
if bearer_type:
bearer_label = bearer_email or bearer_id or "-"
bearer_type = escape(str(bearer_type))
bearer_label = escape(str(bearer_label))
credentials_items += f"""
<li class="list-group-item">
<b>Bearer:</b> {bearer_type} ({bearer_label})
</li>"""
region = getattr(provider.session, "default_region", None)
if region:
region = escape(str(region))
credentials_items += f"""
<li class="list-group-item">
<b>Default Region:</b> {region}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Scaleway Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Scaleway Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_linode_assessment_summary(provider: Provider) -> str:
"""
get_linode_assessment_summary gets the HTML assessment summary for the Linode provider
Args:
provider (Provider): the Linode provider object
Returns:
str: HTML assessment summary for the Linode provider
"""
try:
username = getattr(provider.identity, "username", None) or "-"
email = getattr(provider.identity, "email", None) or "-"
account_id = getattr(provider.identity, "account_id", None) or "-"
username = escape(str(username))
email = escape(str(email))
account_id = escape(str(account_id))
assessment_items = f"""
<li class="list-group-item">
<b>Account ID:</b> {account_id}
</li>
<li class="list-group-item">
<b>Username:</b> {username}
</li>
<li class="list-group-item">
<b>Email:</b> {email}
</li>"""
credentials_items = """
<li class="list-group-item">
<b>Authentication:</b> API Token
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Linode Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Linode Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_huaweicloud_assessment_summary(provider: Provider) -> str:
"""
get_huaweicloud_assessment_summary gets the HTML assessment summary for the Huawei Cloud provider
Args:
provider (Provider): the Huawei Cloud provider object
Returns:
str: HTML assessment summary for the Huawei Cloud provider
"""
try:
profile = (
provider.identity.profile
if provider.identity.profile is not None
else "default"
)
if isinstance(provider.identity.regions, set):
audited_regions = ", ".join(sorted(provider.identity.regions))
elif not provider.identity.regions:
audited_regions = "All Regions"
else:
audited_regions = ", ".join(provider.identity.regions)
account_id = escape(str(provider.identity.account_id))
account_name = escape(str(provider.identity.account_name))
profile = escape(str(profile))
audited_regions = escape(str(audited_regions))
domain_id = escape(str(provider.identity.domain_id))
user_id = escape(str(provider.identity.user_id))
user_name = escape(str(provider.identity.user_name))
identity_type = escape(str(provider.identity.identity_type))
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Huawei Cloud Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Account ID:</b> {account_id}
</li>
<li class="list-group-item">
<b>Account Name:</b> {account_name}
</li>
<li class="list-group-item">
<b>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">
Huawei Cloud Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Domain ID:</b> {domain_id}
</li>
<li class="list-group-item">
<b>User ID:</b> {user_id}
</li>
<li class="list-group-item">
<b>User Name:</b> {user_name}
</li>
<li class="list-group-item">
<b>Identity Type:</b> {identity_type}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_assessment_summary(provider: Provider) -> str:
"""
get_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
# This is based in the Provider_provider class
# It is not pretty but useful
# AWS_provider --> aws
# GCP_provider --> gcp
# GitHub_provider --> github
# Azure_provider --> azure
# Kubernetes_provider --> kubernetes
# Try static method first, fall back to provider method
method_name = f"get_{provider.type}_assessment_summary"
if hasattr(HTML, method_name):
return getattr(HTML, method_name)(provider)
else:
# Dynamic fallback: any external/custom provider
return provider.get_html_assessment_summary()
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""