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
7 changes: 1 addition & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,4 @@ repos:
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: []

- repo: https://github.com/google/osv-scanner
rev: v2.3.1
hooks:
- id: osv-scanner
args: []
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ A framework for code security that provides abstractions for static analysis too
**CodeSecTools** is a collection of scripts and wrappers that abstract external resources (such as SAST tools, datasets, and codebases), providing standardized interfaces to help them interact easily.

<div align="center">
<img src="docs/assets/overview.svg" alt="CodeSecTools Overview" style="width: 75%; height: auto;" />
<img src="docs/assets/workflow.svg" alt="Workflow" style="width: 85%; height: auto;" />
<img src="docs/assets/workflow_example.svg" alt="Workflow" style="width: 85%; height: auto;" />
</div>

For step-by-step instructions on installation, configuration, and basic usage, please refer to the [quick start guide](https://oppida.github.io/CodeSecTools/home/quick_start_guide.html).
For step-by-step instructions on installation, configuration, and basic usage, please refer to the [**quick start guide**](https://oppida.github.io/CodeSecTools/home/quick_start_guide.html).

For more details on the design and integration of SAST tools and datasets in CodeSecTools, please refer to the [documentation](https://oppida.github.io/CodeSecTools).

Expand All @@ -47,9 +48,9 @@ For more details on the design and integration of SAST tools and datasets in Cod

|SAST Tool|Languages|Maintained|Included in Docker|Continuous Testing|Last Test Date|
|:---:|:---:|:---:|:---:|:---:|:---:|
|Coverity|Java|⚠️<br>(Deprioritized)|❌|❌<br>(Proprietary)|October 2025|
|Coverity|C/C++, Java||❌|❌<br>(Proprietary)|February 2026|
|Semgrep Community Edition|C/C++, Java|✅|✅|✅|[Latest PR](https://github.com/OPPIDA/CodeSecTools/actions/workflows/ci.yaml)|
|Snyk Code|C/C++, Java|✅|❌|❌<br>(Rate limited)|November 2025|
|Snyk Code|C/C++, Java|✅|❌|❌<br>(Rate limited)|February 2026|
|Bearer|Java|✅|✅|✅|[Latest PR](https://github.com/OPPIDA/CodeSecTools/actions/workflows/ci.yaml)|
|SpotBugs|Java|✅|✅|✅|[Latest PR](https://github.com/OPPIDA/CodeSecTools/actions/workflows/ci.yaml)|
|Cppcheck|C/C++|✅|✅|✅|[Latest PR](https://github.com/OPPIDA/CodeSecTools/actions/workflows/ci.yaml)|
Expand Down Expand Up @@ -97,10 +98,10 @@ Mount necessary directories if you want to include:
- a target (`-v ./myproject:/home/codesectools/myproject`)
- existing CodeSecTools data (`-v $HOME/.codesectools:/home/codesectools/.codesectools`)

A better way is to use the CLI:
A simpler way is to use the CLI:

```bash
$ cstools -d docker --help
$ cstools docker --help

Usage: cstools docker [OPTIONS]

Expand Down
2 changes: 1 addition & 1 deletion codesectools/datasets/core/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ def validate(self, analysis_results: list[AnalysisResult]) -> GitRepoDatasetData
"fp_cwes": fp_cwes,
"fn_cwes": fn_cwes,
"time": analysis_result.time,
"loc": analysis_result.loc,
"lines_of_codes": analysis_result.lines_of_codes,
}
validated_repos.append(result)

Expand Down
4 changes: 2 additions & 2 deletions codesectools/sasts/all/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def list_() -> None:
"Dataset",
", ".join(
f"[b]{sast.name}[/b]"
for sast in all_sast.sasts
for sast in all_sast.any_sasts
if dataset_full_name in sast.list_results(dataset=True)
),
)
Expand All @@ -187,7 +187,7 @@ def list_() -> None:
"Project",
", ".join(
f"[b]{sast.name}[/b]"
for sast in all_sast.sasts
for sast in all_sast.any_sasts
if project in sast.list_results(project=True)
),
)
Expand Down
48 changes: 24 additions & 24 deletions codesectools/sasts/all/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Graphics(CoreGraphics):
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_color (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.

Expand All @@ -26,12 +26,12 @@ def __init__(self, project_name: str) -> None:
self.project_name = project_name
self.all_sast = AllSAST()
self.output_dir = self.all_sast.output_dir / project_name
self.color_mapping = {}
self.sast_color = {}
cmap = plt.get_cmap("Set2")
self.sast_names = []
for i, sast in enumerate(self.all_sast.sasts):
for i, sast in enumerate(self.all_sast.partial_sasts):
if self.project_name in sast.list_results(project=True):
self.color_mapping[sast.name] = cmap(i)
self.sast_color[sast.name] = cmap(i)
self.sast_names.append(sast.name)
self.plot_functions = []

Expand All @@ -49,11 +49,11 @@ def __init__(self, project_name: str) -> None:
)

def plot_overview(self) -> Figure:
"""Generate an overview plot with stats by files, SAST tools, and categories."""
"""Generate an overview plot with stats by files, SAST tools, and levels."""
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, layout="constrained")
by_files = self.result.stats_by_files()
by_sasts = self.result.stats_by_sasts()
by_categories = self.result.stats_by_categories()
by_levels = self.result.stats_by_levels()

# Plot by files
X_files, Y_files = [], []
Expand All @@ -64,10 +64,10 @@ def plot_overview(self) -> Figure:
X_files.append(shorten_path(k))
Y_files.append(v["count"])

COLORS_COUNT = {v: 0 for k, v in self.color_mapping.items()}
COLORS_COUNT = {v: 0 for k, v in self.sast_color.items()}

for sast in v["sasts"]:
color = self.color_mapping[sast]
for sast_name in v["sasts"]:
color = self.sast_color[sast_name]
COLORS_COUNT[color] += 1

bars = []
Expand Down Expand Up @@ -95,41 +95,41 @@ def plot_overview(self) -> Figure:
ax2.bar(
X_sasts,
Y_checkers,
color=[self.color_mapping[s] for s in X_sasts],
color=[self.sast_color[s] for s in X_sasts],
)
ax2.set_xticks(X_sasts, X_sasts, rotation=45, ha="right")
ax2.set_title("Stats by SAST tools")

# Plot by categories
X_categories = ["HIGH", "MEDIUM", "LOW"]
for category in X_categories:
if not by_categories.get(category):
# Plot by levels
X_levels = ["error", "warning", "note", "none"]
for level in X_levels:
if not by_levels.get(level):
continue

sast_counts = by_categories[category]["sast_counts"]
sast_counts = by_levels[level]["sast_counts"]

bars = []
current_height = 0
for sast_name, count in sorted(sast_counts.items()):
color = self.color_mapping[sast_name]
color = self.sast_color[sast_name]
height = count
if height > 0:
bars.append((category, current_height + height, color))
bars.append((level, current_height + height, color))
current_height += height

for category_name, height, color in bars[::-1]:
ax3.bar(category_name, height, color=color)
for level_name, height, color in bars[::-1]:
ax3.bar(level_name, height, color=color)

ax3.set_xticks(X_categories, X_categories, rotation=45, ha="right")
ax3.set_title("Stats by categories")
ax3.set_xticks(X_levels, X_levels, rotation=45, ha="right")
ax3.set_title("Stats by levels")

fig.suptitle(
f"Project {self.project_name}, {len(self.result.files)} files analyzed, {len(self.result.defects)} defects raised",
fontsize=16,
)
labels = list(self.color_mapping.keys())
labels = list(self.sast_color.keys())
handles = [
plt.Rectangle((0, 0), 1, 1, color=self.color_mapping[label])
plt.Rectangle((0, 0), 1, 1, color=self.sast_color[label])
for label in labels
]
plt.legend(handles, labels)
Expand Down Expand Up @@ -160,7 +160,7 @@ def plot_top_cwes(self) -> Figure:
sast_counts,
bottom=bottoms,
label=sast_name,
color=self.color_mapping.get(sast_name),
color=self.sast_color.get(sast_name),
)
bottoms = [b + c for b, c in zip(bottoms, sast_counts, strict=False)]

Expand Down
66 changes: 28 additions & 38 deletions codesectools/sasts/all/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ class AllSASTAnalysisResult:
"""Represent the aggregated results from multiple SAST analyses on a single project."""

def __init__(self, name: str, analysis_results: dict[str, AnalysisResult]) -> None:
"""Initialize an AllSASTAnalysisResult instance."""
"""Initialize an AllSASTAnalysisResult instance.

Args:
name: The name of the project.
analysis_results: A dictionary of analysis results from various SAST tools.

"""
self.name = name
self.source_path = None
self.analysis_results = analysis_results
Expand All @@ -34,22 +40,6 @@ def __init__(self, name: str, analysis_results: dict[str, AnalysisResult]) -> No
self.files |= set(analysis_result.files)
self.defects += analysis_result.defects

self.category_mapping = {}
for sast_name in self.sast_names:
sast = SASTS_ALL[sast_name]["sast"]
for category_name, color in sast.color_mapping.items():
if color.lower() == "red":
self.category_mapping[(sast_name, category_name)] = "HIGH"
elif color.lower() == "orange":
self.category_mapping[(sast_name, category_name)] = "MEDIUM"
elif color.lower() == "yellow":
self.category_mapping[(sast_name, category_name)] = "LOW"

for defect in self.defects:
defect.category = self.category_mapping.get(
(defect.sast, defect.category), "LOW"
)

def __repr__(self) -> str:
"""Return a developer-friendly string representation of the aggregated result."""
return f"""{self.__class__.__name__}(
Expand Down Expand Up @@ -78,9 +68,9 @@ def stats_by_files(self) -> dict:
stats = {}
for defect in self.defects:
if defect.filepath_str not in stats.keys():
stats[defect.filepath_str] = {"count": 1, "sasts": [defect.sast]}
stats[defect.filepath_str] = {"count": 1, "sasts": [defect.sast_name]}
else:
stats[defect.filepath_str]["sasts"].append(defect.sast)
stats[defect.filepath_str]["sasts"].append(defect.sast_name)
stats[defect.filepath_str]["count"] += 1

return stats
Expand All @@ -89,23 +79,23 @@ def stats_by_sasts(self) -> dict:
"""Calculate statistics on defects, grouped by SAST tool."""
stats = {}
for defect in self.defects:
if defect.sast not in stats.keys():
stats[defect.sast] = {"count": 1}
if defect.sast_name not in stats.keys():
stats[defect.sast_name] = {"count": 1}
else:
stats[defect.sast]["count"] += 1
stats[defect.sast_name]["count"] += 1

return stats

def stats_by_categories(self) -> dict:
"""Calculate statistics on defects, grouped by severity category."""
def stats_by_levels(self) -> dict:
"""Calculate statistics on defects, grouped by severity level."""
stats = {}
for defect in self.defects:
if defect.category not in stats.keys():
stats[defect.category] = {"count": 0, "sast_counts": {}}
if defect.level not in stats.keys():
stats[defect.level] = {"count": 0, "sast_counts": {}}

stats[defect.category]["count"] += 1
sast_counts = stats[defect.category]["sast_counts"]
sast_counts[defect.sast] = sast_counts.get(defect.sast, 0) + 1
stats[defect.level]["count"] += 1
sast_counts = stats[defect.level]["sast_counts"]
sast_counts[defect.sast_name] = sast_counts.get(defect.sast_name, 0) + 1
return stats

def stats_by_cwes(self) -> dict:
Expand All @@ -119,14 +109,14 @@ def stats_by_cwes(self) -> dict:
stats[defect.cwe] = {
"count": 1,
"files": [defect.filepath_str],
"sast_counts": {defect.sast: 1},
"sast_counts": {defect.sast_name: 1},
}
else:
stats[defect.cwe]["count"] += 1
if defect.filepath_str not in stats[defect.cwe]["files"]:
stats[defect.cwe]["files"].append(defect.filepath_str)
stats[defect.cwe]["sast_counts"][defect.sast] = (
stats[defect.cwe]["sast_counts"].get(defect.sast, 0) + 1
stats[defect.cwe]["sast_counts"][defect.sast_name] = (
stats[defect.cwe]["sast_counts"].get(defect.sast_name, 0) + 1
)
return stats

Expand All @@ -144,7 +134,7 @@ def stats_by_scores(self) -> dict:

defects_same_cwe = 0
for cwe in defects_cwes:
cwes_sasts = {d.sast for d in defects if d.cwe == cwe}
cwes_sasts = {d.sast_name for d in defects if d.cwe == cwe}
if set(self.sast_names) == cwes_sasts:
defects_same_cwe += 1
else:
Expand All @@ -162,7 +152,7 @@ def stats_by_scores(self) -> dict:
defects_same_location = 0
defects_same_location_same_cwe = 0
for _, defects_ in defect_locations.items():
if set(defect.sast for defect in defects_) == set(self.sast_names):
if set(defect.sast_name for defect in defects_) == set(self.sast_names):
defects_same_location += 1
defects_by_cwe = {}
for defect in defects_:
Expand All @@ -171,14 +161,14 @@ def stats_by_scores(self) -> dict:
defects_by_cwe[defect.cwe].append(defect)

for _, defects_ in defects_by_cwe.items():
if set(defect.sast for defect in defects_) == set(
if set(defect.sast_name for defect in defects_) == set(
self.sast_names
):
defects_same_location_same_cwe += 1
else:
defects_same_location_same_cwe += (
len(
set(defect.sast for defect in defects_)
set(defect.sast_name for defect in defects_)
& set(self.sast_names)
)
- 1
Expand Down Expand Up @@ -224,7 +214,7 @@ def prepare_report_data(self) -> dict:
for group in group_successive(defect.lines):
start, end = group[0], group[-1]
locations.append(
(defect.sast, defect.cwe, defect.message, (start, end))
(defect.sast_name, defect.cwe, defect.message, (start, end))
)

report["files"][defect_file] = {
Expand All @@ -239,7 +229,7 @@ def prepare_report_data(self) -> dict:
k: v
for k, v in sorted(
report["files"].items(),
key=lambda item: (sum(v for v in item[1]["score"].values())),
key=lambda item: sum(v for v in item[1]["score"].values()),
reverse=True,
)
}
Expand Down
2 changes: 1 addition & 1 deletion codesectools/sasts/all/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def generate_single_defect(self, file_data: dict) -> tuple:
else "None"
)
rows.append(
(start, shortcut, defect.sast, cwe_link, defect.message)
(start, shortcut, defect.sast_name, cwe_link, defect.message)
)
else:
cwe_link = (
Expand Down
17 changes: 13 additions & 4 deletions codesectools/sasts/all/sast.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,24 @@ class AllSAST:
def __init__(self) -> None:
"""Initialize the AllSAST instance."""
self.output_dir = USER_OUTPUT_DIR / self.name
self.sasts: list[SAST] = []
self.full_sasts: list[SAST] = []
self.partial_sasts: list[SAST] = []
self.any_sasts: list[SAST] = []
for _, sast_data in SASTS_ALL.items():
if sast_data["status"] == "full":
self.sasts.append(sast_data["sast"]())
self.full_sasts.append(sast_data["sast"]())
self.partial_sasts.append(sast_data["sast"]())
self.any_sasts.append(sast_data["sast"]())
elif sast_data["status"] == "partial":
self.partial_sasts.append(sast_data["sast"]())
self.any_sasts.append(sast_data["sast"]())
else:
self.any_sasts.append(sast_data["sast"]())

self.sasts_by_lang = {}
self.sasts_by_dataset = {}

for sast in self.sasts:
for sast in self.full_sasts:
for lang in sast.supported_languages:
if self.sasts_by_lang.get(lang):
self.sasts_by_lang[lang].append(sast)
Expand All @@ -45,7 +54,7 @@ def list_results(
) -> set[str]:
"""List the names of analysis results common to all enabled SAST tools."""
output_dirs = set()
for sast in self.sasts:
for sast in self.partial_sasts:
if not output_dirs:
output_dirs = set(
sast.list_results(project=project, dataset=dataset, limit=limit)
Expand Down
Loading