Skip to content

Commit

Permalink
Support for pdf reports (#164)
Browse files Browse the repository at this point in the history
* Support for pdf reports

Signed-off-by: Prabhu Subramanian <[email protected]>

* Remove console.rule

Signed-off-by: Prabhu Subramanian <[email protected]>

* Improved js explanations

Signed-off-by: Prabhu Subramanian <[email protected]>

---------

Signed-off-by: Prabhu Subramanian <[email protected]>
  • Loading branch information
prabhu authored Nov 15, 2023
1 parent 118d4f5 commit 692c532
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 34 deletions.
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,19 @@ RUN set -e; \
esac; \
echo -e "[nodejs]\nname=nodejs\nstream=21\nprofiles=\nstate=enabled\n" > /etc/dnf/modules.d/nodejs.module \
&& microdnf module enable php ruby -y \
&& microdnf install -y php php-curl php-zip php-bcmath php-json php-pear php-mbstring php-devel make gcc git-core python3.11 python3.11-devel python3.11-pip ruby ruby-devel \
&& microdnf install -y php php-curl php-zip php-bcmath php-json php-pear php-mbstring php-devel make gcc git-core \
python3.11 python3.11-devel python3.11-pip ruby ruby-devel \
libX11-devel libXext-devel libXrender-devel libjpeg-turbo-devel \
pcre2 which tar zip unzip sudo nodejs ncurses glibc-common glibc-all-langpacks xorg-x11-fonts-75dpi xorg-x11-fonts-Type1 \
&& alternatives --install /usr/bin/python3 python /usr/bin/python3.11 1 \
&& python3 --version \
&& python3 -m pip install --upgrade pip \
&& curl -LO https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox-0.12.6.1-2.almalinux9.${ARCH_NAME}.rpm \
&& rpm -ivh wkhtmltox-0.12.6.1-2.almalinux9.${ARCH_NAME}.rpm \
&& rm wkhtmltox-0.12.6.1-2.almalinux9.${ARCH_NAME}.rpm \
&& curl -s "https://get.sdkman.io" | bash \
&& source "$HOME/.sdkman/bin/sdkman-init.sh" \
&& echo -e "sdkman_auto_answer=true\nsdkman_selfupdate_feature=false\nsdkman_auto_env=true" >> $HOME/.sdkman/etc/config \
&& echo -e "sdkman_auto_answer=true\nsdkman_selfupdate_feature=false\nsdkman_auto_env=true\nsdkman_curl_connect_timeout=20\nsdkman_curl_max_time=0" >> $HOME/.sdkman/etc/config \
&& sdk install java $JAVA_VERSION \
&& sdk install maven $MAVEN_VERSION \
&& sdk install gradle $GRADLE_VERSION \
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,10 @@ The license data is sourced from choosealicense.com and is quite limited. If the

dep-scan could auto-detect most cloud applications and Kubernetes manifest files. Pass the argument `-t yaml-manifest` to manually specify the type.

## PDF reports

Ensure [wkhtmltopdf](https://wkhtmltopdf.org/downloads.html) is installed or use the official container image to generate pdf reports. Use with `--explain` for more detailed reports.

## Discord support

The developers could be reached via the [discord](https://discord.gg/DCNxzaeUpd) channel for enterprise support.
Expand Down
18 changes: 10 additions & 8 deletions depscan/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import oras.client
from quart import Quart, request
from rich.panel import Panel
from rich.terminal_theme import MONOKAI
from rich.terminal_theme import DEFAULT_TERMINAL_THEME, MONOKAI
from vdb.lib import config
from vdb.lib import db as db_lib
from vdb.lib.config import data_dir
Expand All @@ -19,16 +19,16 @@
from vdb.lib.osv import OSVSource
from vdb.lib.utils import parse_purl

from depscan.lib import github, utils, explainer
from depscan.lib import explainer, github, utils
from depscan.lib.analysis import (
PrepareVdrOptions,
analyse_licenses,
analyse_pkg_risks,
find_purl_usages,
jsonl_report,
prepare_vdr,
suggest_version,
summary_stats,
find_purl_usages,
)
from depscan.lib.audit import audit, risk_audit, risk_audit_map, type_audit_map
from depscan.lib.bom import create_bom, get_pkg_by_type, get_pkg_list, submit_bom
Expand Down Expand Up @@ -665,6 +665,7 @@ def main():
else os.path.join(reports_dir, "depscan.json")
)
html_file = areport_file.replace(".json", ".html")
pdf_file = areport_file.replace(".json", ".pdf")
# Create reports directory
if reports_dir and not os.path.exists(reports_dir):
os.makedirs(reports_dir, exist_ok=True)
Expand All @@ -686,7 +687,6 @@ def main():
results = []
report_file = areport_file.replace(".json", f"-{project_type}.json")
risk_report_file = areport_file.replace(".json", f"-risk.{project_type}.json")
console.rule(style="gray37")
if args.bom and os.path.exists(args.bom):
bom_file = args.bom
creation_status = True
Expand Down Expand Up @@ -732,7 +732,6 @@ def main():
analyse_licenses(project_type, licenses_results, license_report_file)
if project_type in risk_audit_map:
if args.risk_audit:
console.rule(style="gray37")
console.print(
Panel(
f"Performing OSS Risk Audit for packages from "
Expand Down Expand Up @@ -764,12 +763,11 @@ def main():
"Depscan supports OSS Risk audit for this "
"project.\nTo enable set the environment variable ["
"bold]ENABLE_OSS_RISK=true[/bold]",
title="New Feature",
title="Risk Audit Capability",
expand=False,
)
)
if project_type in type_audit_map:
console.rule(style="gray37")
LOG.debug(
"Performing remote audit for %s of type %s",
src_dir,
Expand Down Expand Up @@ -897,7 +895,11 @@ def main():
direct_purls=direct_purls,
reached_purls=reached_purls,
)
console.save_html(html_file, theme=MONOKAI)
console.save_html(
html_file,
theme=MONOKAI if os.getenv("USE_DARK_THEME") else DEFAULT_TERMINAL_THEME,
)
utils.export_pdf(html_file, pdf_file)
# Submit vdr/vex files to threatdb server
if args.threatdb_server and (args.threatdb_username or args.threatdb_token):
submit_bom(
Expand Down
5 changes: 2 additions & 3 deletions depscan/lib/analysis.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import os.path
from collections import defaultdict, OrderedDict
from collections import OrderedDict, defaultdict
from dataclasses import dataclass
from typing import Dict, List, Optional

Expand All @@ -11,7 +11,7 @@
from rich.tree import Tree
from vdb.lib import CPE_FULL_REGEX
from vdb.lib.config import placeholder_fix_version
from vdb.lib.utils import parse_purl, parse_cpe
from vdb.lib.utils import parse_cpe, parse_purl

from depscan.lib import config
from depscan.lib.logger import LOG, console
Expand Down Expand Up @@ -587,7 +587,6 @@ def prepare_vdr(options: PrepareVdrOptions):
}
)
if not options.no_vuln_table:
console.rule(style="gray37")
console.print()
console.print(table)
console.print()
Expand Down
2 changes: 1 addition & 1 deletion depscan/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,4 +433,4 @@ def get_int_from_env(name, default):

max_reachable_explanations = get_int_from_env("max_reachable_explanations", 20)

max_reachable_explanations_purl = get_int_from_env("max_reachable_explanations_purl", 5)
max_purl_per_flow = get_int_from_env("max_purl_per_flow", 6)
53 changes: 35 additions & 18 deletions depscan/lib/explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
from rich.table import Table
from rich.tree import Tree

from depscan.lib.config import max_purl_per_flow, max_reachable_explanations
from depscan.lib.logger import console
from depscan.lib.config import (
max_reachable_explanations,
)


def explain(
Expand Down Expand Up @@ -95,7 +93,7 @@ def explain_reachables(reachables, pkg_group_rows, project_type):
if checked_flows:
console.print(
Panel(
"Review the detected validation/sanitization methods. Refactor the application to centralize the common valiidation operations to improve the security posture.",
"Review the detected validation/sanitization methods. Refactor the application to validate using custom middlewares to improve the security posture.",
title="Recommendation",
expand=False,
)
Expand Down Expand Up @@ -135,7 +133,22 @@ def flow_to_source_sink(flow, purls, project_type):
source_sink_desc = flow.get("code").split("\n")[0]
elif project_type not in ("java") and flow.get("label") == "IDENTIFIER":
source_sink_desc = flow.get("code").split("\n")[0]
if len(purls) == 1:
# Try to understand the source a bit more
if source_sink_desc.startswith("require("):
source_sink_desc = "Flow starts from a module import"
elif (
".use(" in source_sink_desc
or ".subscribe(" in source_sink_desc
or ".on(" in source_sink_desc
or ".emit(" in source_sink_desc
or " => {" in source_sink_desc
):
source_sink_desc = "Flow starts from a callback function"
elif (
"middleware" in source_sink_desc.lower() or "route" in source_sink_desc.lower()
):
source_sink_desc = "Flow starts from a middlware"
elif len(purls) == 1:
source_sink_desc = f"{source_sink_desc} can be used to reach this package."
else:
source_sink_desc = (
Expand All @@ -144,7 +157,7 @@ def flow_to_source_sink(flow, purls, project_type):
return source_sink_desc


def flow_to_str(flow):
def flow_to_str(flow, project_type):
""""""
has_check_tag = False
file_loc = ""
Expand All @@ -162,11 +175,12 @@ def flow_to_str(flow):
param_name = ""
node_desc = f'{flow.get("parentMethodName")}([red]{param_name}[/red]) :right_arrow_curving_left:'
if tags:
node_desc = (
f"{node_desc}\n[bold]Tags :label: [/bold] [italic]{tags}[/italic]\n"
)
elif flow.get("label") == "IDENTIFIER" and node_desc.startswith("<"):
node_desc = flow.get("name")
node_desc = f"{node_desc}\n[bold]Tags:[/bold] [italic]{tags}[/italic]\n"
elif flow.get("label") == "IDENTIFIER":
if node_desc.startswith("<"):
node_desc = flow.get("name")
if project_type not in ("java") and tags:
node_desc = f"{node_desc}\n[bold]Tags:[/bold] [italic]{tags}[/italic]\n"
if flow.get("tags"):
if (
"validation" in tags
Expand All @@ -175,16 +189,15 @@ def flow_to_str(flow):
or "sanitize" in tags
):
has_check_tag = True
elif flow.get("label") in ("CALL", "RETURN"):
elif flow.get("label") in ("CALL", "RETURN") or project_type not in ("java"):
code = flow.get("code", "").lower()
# Let's broaden and look for more check method patterns
# This is not a great logic but since we're offering some ideas this should be ok
# Hopefully, the tagger would improve to handle these cases in the future
if (
"escape(" in code
or "encode(" in code
or "encrypt(" in code
or "validate" in code
re.search("(escape|encode|encrypt|validate|sanitize)", code)
or "authorize" in node_desc.lower()
or "authenticate" in node_desc.lower()
):
has_check_tag = True
if has_check_tag:
Expand All @@ -196,6 +209,10 @@ def explain_flows(flows, purls, project_type):
""""""
tree = None
comments = []
if len(purls) > max_purl_per_flow:
comments.append(
":exclamation_mark: Refactor this flow to reduce the number of external libraries used."
)
purls_str = "\n".join(purls)
comments.append(f"Reachable Packages:\n{purls_str}")
added_flows = []
Expand All @@ -211,7 +228,7 @@ def explain_flows(flows, purls, project_type):
continue
if not source_sink_desc:
source_sink_desc = flow_to_source_sink(aflow, purls, project_type)
file_loc, flow_str, has_check_tag_flow = flow_to_str(aflow)
file_loc, flow_str, has_check_tag_flow = flow_to_str(aflow, project_type)
if last_file_loc == file_loc:
continue
last_file_loc = file_loc
Expand All @@ -227,6 +244,6 @@ def explain_flows(flows, purls, project_type):
if has_check_tag:
comments.insert(
0,
":white_medium_small_square: Check if the mitigation used in this flow is valid and appropriate for your security requirements.",
":white_medium_small_square: Check if the mitigation(s) used in this flow is valid and appropriate for your security requirements.",
)
return tree, "\n".join(comments), source_sink_desc, has_check_tag
3 changes: 2 additions & 1 deletion depscan/lib/github.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import httpx
import os

import httpx
from github import Auth, Github

from depscan.lib import config
Expand Down
34 changes: 34 additions & 0 deletions depscan/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import re
from collections import defaultdict
from datetime import datetime
from importlib.metadata import distribution

from vdb.lib import db as db_lib
Expand Down Expand Up @@ -374,3 +375,36 @@ def get_version():
Returns the version of depscan
"""
return distribution("owasp-depscan").version


def export_pdf(
html_file,
pdf_file,
title="DepScan Analysis",
footer=f'Report generated by OWASP dep-scan at {datetime.now().strftime("%B %d, %Y %H:%M")}',
):
"""
Method to export html as pdf using pdfkit
"""
pdf_options = {
"page-size": "A2",
"margin-top": "0.5in",
"margin-right": "0.25in",
"margin-bottom": "0.5in",
"margin-left": "0.25in",
"encoding": "UTF-8",
"outline": None,
"title": title,
"footer-right": footer,
"minimum-font-size": "12",
"disable-smart-shrinking": "",
}
try:
import pdfkit

if not pdf_file and html_file:
pdf_file = html_file.replace(".html", ".pdf")
if os.path.exists(html_file):
pdfkit.from_file(html_file, pdf_file, options=pdf_options)
except Exception as e:
pass
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ authors = [
{name = "Team AppThreat", email = "[email protected]"},
]
dependencies = [
"appthreat-vulnerability-db>=5.5.1",
"appthreat-vulnerability-db>=5.5.2",
"defusedxml",
"oras",
"PyYAML",
"rich",
"quart",
"PyGithub",
"toml",
"pdfkit",
]

requires-python = ">=3.8"
Expand Down

0 comments on commit 692c532

Please sign in to comment.