Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions codesectools/sasts/all/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import typer
from click import Choice
from rich import print
from typing_extensions import Annotated
from typing_extensions import Annotated, Literal

from codesectools.datasets import DATASETS_ALL
from codesectools.datasets.core.dataset import FileDataset, GitRepoDataset
Expand Down Expand Up @@ -200,26 +200,16 @@ def plot(
help="Overwrite existing figures",
),
] = False,
show: Annotated[
bool,
typer.Option(
"--show",
help="Display figures",
),
] = False,
pgf: Annotated[
bool,
typer.Option(
"--pgf",
help="Export figures to pgf format (for LaTeX document)",
),
] = False,
format: Annotated[
Literal["png", "pdf", "svg"],
typer.Option("--format", help="Figures export format"),
] = "png",
) -> None:
"""Generate and display plots for a project's aggregated analysis results."""
from codesectools.sasts.all.graphics import ProjectGraphics

project_graphics = ProjectGraphics(project_name=project)
project_graphics.export(overwrite=overwrite, show=show, pgf=pgf)
project_graphics.export(overwrite=overwrite, format=format)

@cli.command(help="Generate an HTML report")
def report(
Expand Down
82 changes: 12 additions & 70 deletions codesectools/sasts/all/graphics.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
"""Provides classes for generating plots and visualizations from aggregated SAST results."""

import shutil
import tempfile

import matplotlib
import matplotlib.pyplot as plt
import typer
from matplotlib.figure import Figure
from rich import print

from codesectools.sasts.all.sast import AllSAST
from codesectools.sasts.core.graphics import Graphics as CoreGraphics
from codesectools.utils import shorten_path

## Matplotlib config
matplotlib.rcParams.update(
{
"font.family": "serif",
"font.size": 11,
}
)

class Graphics(CoreGraphics):
"""Base class for generating plots for aggregated SAST results.

Attributes:
project_name (str): The name of the project being visualized.
all_sast (AllSAST): The instance managing all SAST tools.
output_dir (Path): The directory containing the aggregated results.
color_mapping (dict): A dictionary mapping SAST tool names to colors.
sast_names (list[str]): A list of names of the SAST tools involved in the analysis.
plot_functions (list): A list of methods responsible for generating plots.

class Graphics:
"""Base class for generating graphics from aggregated SAST results."""
"""

def __init__(self, project_name: str) -> None:
"""Initialize the Graphics object."""
Expand All @@ -38,61 +35,6 @@ def __init__(self, project_name: str) -> None:
self.sast_names.append(sast.name)
self.plot_functions = []

# Plot options
self.limit = 10

self.has_latex = shutil.which("pdflatex")
if self.has_latex:
matplotlib.use("pgf")
matplotlib.rcParams.update(
{
"pgf.texsystem": "pdflatex",
"text.usetex": True,
"pgf.rcfonts": False,
}
)
else:
print("pdflatex not found, pgf will not be generated")

def export(self, overwrite: bool, pgf: bool, show: bool) -> None:
"""Generate, save, and optionally display all registered plots.

Args:
overwrite: If True, overwrite existing figure files.
pgf: If True and LaTeX is available, export figures in PGF format.
show: If True, open the generated figures using the default viewer.

"""
for plot_function in self.plot_functions:
fig = plot_function()
fig_name = plot_function.__name__.replace("plot_", "")
fig.set_size_inches(12, 7)

if show:
with tempfile.NamedTemporaryFile(delete=True) as temp:
fig.savefig(f"{temp.name}.png", bbox_inches="tight")
typer.launch(f"{temp.name}.png", wait=False)

figure_dir = self.output_dir / "_figures"
figure_dir.mkdir(exist_ok=True, parents=True)
figure_path = figure_dir / f"{fig_name}.png"
if figure_path.is_file() and not overwrite:
if not typer.confirm(
f"Found existing figure at {figure_path}, would you like to overwrite?"
):
print(f"Figure {fig_name} not saved")
continue

fig.savefig(figure_path, bbox_inches="tight")
print(f"Figure {fig_name} saved at {figure_path}")

if pgf and self.has_latex:
figure_path_pgf = figure_dir / f"{fig_name}.pgf"
fig.savefig(figure_path_pgf, bbox_inches="tight")
print(f"Figure {fig_name} exported to pgf")

plt.close(fig)


## Single project
class ProjectGraphics(Graphics):
Expand Down
40 changes: 9 additions & 31 deletions codesectools/sasts/core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import typer
from click import Choice
from rich import print
from typing_extensions import Annotated
from typing_extensions import Annotated, Literal

from codesectools.datasets import DATASETS_ALL
from codesectools.datasets.core.dataset import FileDataset, GitRepoDataset
Expand Down Expand Up @@ -314,30 +314,12 @@ def plot(
help="Overwrite existing figures",
),
] = False,
show: Annotated[
bool,
typer.Option(
"--show",
help="Display figures",
),
] = False,
pgf: Annotated[
bool,
typer.Option(
"--pgf",
help="Export figures to pgf format (for LaTeX document)",
),
] = False,
format: Annotated[
Literal["png", "pdf", "svg"],
typer.Option("--format", help="Figures export format"),
] = "png",
) -> None:
"""Generate and export plots for a given project or dataset result.

Args:
result: The name of the analysis result to plot.
overwrite: If True, overwrite existing figure files.
show: If True, display the generated figures.
pgf: If True, export figures in PGF format for LaTeX documents.

"""
"""Generate and export plots for a given project or dataset result."""
from codesectools.sasts.core.graphics import (
FileDatasetGraphics,
GitRepoDatasetGraphics,
Expand All @@ -347,7 +329,7 @@ def plot(
if result in self.sast.list_results(project=True):
project = result
project_graphics = ProjectGraphics(self.sast, project_name=project)
project_graphics.export(overwrite=overwrite, show=show, pgf=pgf)
project_graphics.export(overwrite=overwrite, format=format)
elif result in self.sast.list_results(dataset=True):
dataset = result
dataset_name, lang = dataset.split("_")
Expand All @@ -356,15 +338,11 @@ def plot(
file_dataset_graphics = FileDatasetGraphics(
self.sast, dataset=dataset
)
file_dataset_graphics.export(
overwrite=overwrite, show=show, pgf=pgf
)
file_dataset_graphics.export(overwrite=overwrite, format=format)
elif isinstance(dataset, GitRepoDataset):
git_repo_dataset_graphics = GitRepoDatasetGraphics(
self.sast, dataset=dataset
)
git_repo_dataset_graphics.export(
overwrite=overwrite, show=show, pgf=pgf
)
git_repo_dataset_graphics.export(overwrite=overwrite, format=format)
else:
print("Not supported yet")
71 changes: 21 additions & 50 deletions codesectools/sasts/core/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
benchmark performance.
"""

import shutil
import tempfile

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -20,28 +17,24 @@
from codesectools.shared.cwe import CWE
from codesectools.utils import shorten_path

## Matplotlib config
matplotlib.rcParams.update(
{
"font.family": "serif",
"font.size": 11,
}
)


class Graphics:
"""Base class for generating graphics from SAST results.
"""Base class for generating plots and visualizations from SAST results.

Attributes:
sast (SAST): The SAST tool instance.
output_dir (Path): The directory containing the analysis results.
color_mapping (dict): A mapping of categories to colors for plotting.
plot_functions (list): A list of methods that generate plots.
limit (int): The maximum number of items to show in top-N plots.
has_latex (bool): True if a LaTeX installation is found.
limit (int): The maximum number of items to display in charts (default is 10).
filetypes (dict[str, str]): A mapping of file extensions to matplotlib backends.
sast (SAST): The SAST tool instance associated with the graphics.
output_dir (Path): The directory where the analysis results are stored.
color_mapping (dict): A dictionary mapping categories to colors for plots.
plot_functions (list): A list of methods responsible for generating plots.

"""

limit = 10

filetypes = {"png": "AGG", "pdf": "PDF", "svg": "SVG"}

def __init__(self, sast: SAST, project_name: str) -> None:
"""Initialize the Graphics object.

Expand All @@ -56,44 +49,27 @@ def __init__(self, sast: SAST, project_name: str) -> None:
self.color_mapping["NONE"] = "BLACK"
self.plot_functions = []

# Plot options
self.limit = 10

self.has_latex = shutil.which("pdflatex")
if self.has_latex:
matplotlib.use("pgf")
matplotlib.rcParams.update(
{
"pgf.texsystem": "pdflatex",
"text.usetex": True,
"pgf.rcfonts": False,
}
)
else:
print("pdflatex not found, pgf will not be generated")

def export(self, overwrite: bool, pgf: bool, show: bool) -> None:
"""Generate, save, and optionally display all registered plots.
def export(self, overwrite: bool, format: str) -> None:
"""Generate and save the configured plots to the output directory.

Iterates through the registered plot functions, generates the figures,
and saves them to a `_figures` subdirectory within the output directory.

Args:
overwrite: If True, overwrite existing figure files.
pgf: If True and LaTeX is available, export figures in PGF format.
show: If True, open the generated figures using the default viewer.
overwrite: If True, overwrite existing figure files without prompting.
format: The file format for the exported figures (e.g., "png", "pdf", "svg").

"""
matplotlib.use(self.filetypes[format])

for plot_function in self.plot_functions:
fig = plot_function()
fig_name = plot_function.__name__.replace("plot_", "")
fig.set_size_inches(12, 7)

if show:
with tempfile.NamedTemporaryFile(delete=True) as temp:
fig.savefig(f"{temp.name}.png", bbox_inches="tight")
typer.launch(f"{temp.name}.png", wait=False)

figure_dir = self.output_dir / "_figures"
figure_dir.mkdir(exist_ok=True, parents=True)
figure_path = figure_dir / f"{fig_name}.png"
figure_path = figure_dir / f"{fig_name}.{format}"
if figure_path.is_file() and not overwrite:
if not typer.confirm(
f"Found existing figure at {figure_path}, would you like to overwrite?"
Expand All @@ -104,11 +80,6 @@ def export(self, overwrite: bool, pgf: bool, show: bool) -> None:
fig.savefig(figure_path, bbox_inches="tight")
print(f"Figure {fig_name} saved at {figure_path}")

if pgf and self.has_latex:
figure_path_pgf = figure_dir / f"{fig_name}.pgf"
fig.savefig(figure_path_pgf, bbox_inches="tight")
print(f"Figure {fig_name} exported to pgf")

plt.close(fig)


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "CodeSecTools"
version = "0.13.4"
version = "0.13.5"
description = "A framework for code security that provides abstractions for static analysis tools and datasets to support their integration, testing, and evaluation."
readme = "README.md"
license = "AGPL-3.0-only"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.