Skip to content

Commit 5ef6410

Browse files
authored
Merge pull request #7 from OPPIDA/feat/c-support
2 parents d25f277 + 48c9026 commit 5ef6410

28 files changed

Lines changed: 117 additions & 65 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ test-debug: ## Spawn an interactive shell in the test container to debug
2828
@docker compose run --rm test /bin/bash
2929

3030
docs-serve: ## Serve the documentation locally
31-
@mkdocs serve
31+
@mkdocs serve --livereload

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A framework for code security that provides abstractions for static analysis too
2424

2525
- Clone the repository:
2626
```bash
27-
git clone git@github.com:OPPIDA/CodeSecTools.git
27+
git clone https://github.com/OPPIDA/CodeSecTools.git
2828
cd CodeSecTools
2929
```
3030

@@ -53,6 +53,16 @@ cd CodeSecTools
5353
- **Concurrent Analysis for Cross-Verification**: CodeSecTools can run multiple SAST tools simultaneously on the same project. This allows for the aggregation and cross-verification of results, increasing confidence in the identified vulnerabilities by highlighting findings reported by multiple tools.
5454
- **Automated Reporting and Visualization**: The framework can generate detailed reports in HTML format and create graphs to visualize analysis results, helping to identify trends such as the most common CWEs or the files with the highest number of defects.
5555

56+
### SAST Tool Integration Status
57+
58+
|SAST Tool|Languages|Maintained|Tested|
59+
|:---:|:---:|:---:|:---:|
60+
|Coverity|Java|❌ (Proprietary)|❌ (Proprietary)|
61+
|Semgrep Community Edition|C, Java|||
62+
|Snyk Code|C, Java||❌ (Rate limited)|
63+
|Bearer|Java|||
64+
|SpotBugs|Java|||
65+
5666
## Usage
5767

5868
#### Command-line interface

codesectools/sasts/all/cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def analyze(
6666
lang: Annotated[
6767
str,
6868
typer.Argument(
69-
click_type=Choice(all_sast.supported_languages),
69+
click_type=Choice(all_sast.sasts_by_lang.keys()),
7070
help="Source code language (only one at the time)",
7171
metavar="LANG",
7272
),
@@ -89,7 +89,7 @@ def analyze(
8989
] = False,
9090
) -> None:
9191
"""Run analysis on the current project with all available SAST tools."""
92-
for sast in all_sast.sasts:
92+
for sast in all_sast.sasts_by_lang.get(lang, []):
9393
if isinstance(sast, PrebuiltSAST) and artifact_dir is None:
9494
print(f"{sast.name} required pre-built artifacts for analysis")
9595
print(
@@ -117,7 +117,7 @@ def benchmark(
117117
dataset: Annotated[
118118
str,
119119
typer.Argument(
120-
click_type=Choice(all_sast.supported_dataset_full_names),
120+
click_type=Choice([d.name for d in all_sast.sasts_by_dataset]),
121121
metavar="DATASET",
122122
),
123123
],
@@ -138,7 +138,7 @@ def benchmark(
138138
) -> None:
139139
"""Run a benchmark on a dataset using all available SAST tools."""
140140
dataset_name, lang = dataset.split("_")
141-
for sast in all_sast.sasts:
141+
for sast in all_sast.sasts_by_dataset.get(lang, []):
142142
dataset = DATASETS_ALL[dataset_name](lang)
143143
if isinstance(dataset, FileDataset):
144144
sast.analyze_files(dataset, overwrite, testing)

codesectools/sasts/all/graphics.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ def __init__(self, project_name: str) -> None:
3131
self.output_dir = self.all_sast.output_dir / project_name
3232
self.color_mapping = {}
3333
cmap = plt.get_cmap("Set2")
34+
self.sast_names = []
3435
for i, sast in enumerate(self.all_sast.sasts):
35-
self.color_mapping[sast.name] = cmap(i)
36+
if self.project_name in sast.list_results(project=True):
37+
self.color_mapping[sast.name] = cmap(i)
38+
self.sast_names.append(sast.name)
3639
self.plot_functions = []
3740

3841
# Plot options
@@ -156,6 +159,9 @@ def plot_overview(self) -> Figure:
156159
# Plot by categories
157160
X_categories = ["HIGH", "MEDIUM", "LOW"]
158161
for category in X_categories:
162+
if not by_categories.get(category):
163+
continue
164+
159165
sast_counts = by_categories[category]["sast_counts"]
160166

161167
bars = []
@@ -200,10 +206,9 @@ def plot_top_cwes(self) -> Figure:
200206
X_cwes.append(f"{cwe.name}")
201207
cwe_data.append(data)
202208

203-
sast_names = sorted([sast.name for sast in self.all_sast.sasts])
204209
bottoms = [0] * len(X_cwes)
205210

206-
for sast_name in sast_names:
211+
for sast_name in self.sast_names:
207212
sast_counts = [data["sast_counts"].get(sast_name, 0) for data in cwe_data]
208213

209214
ax.bar(

codesectools/sasts/all/sast.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from typing import TYPE_CHECKING
44

5-
from codesectools.datasets import DATASETS_ALL
65
from codesectools.sasts import SASTS_ALL
76
from codesectools.sasts.all.parser import AllSASTAnalysisResult
87
from codesectools.utils import USER_OUTPUT_DIR
@@ -25,31 +24,21 @@ def __init__(self) -> None:
2524
if sast_data["status"] == "full":
2625
self.sasts.append(sast_data["sast"]())
2726

28-
self.supported_languages = {}
29-
self.supported_dataset_names = {}
27+
self.sasts_by_lang = {}
28+
self.sasts_by_dataset = {}
3029

3130
for sast in self.sasts:
32-
if not self.supported_languages:
33-
self.supported_languages = set(sast.supported_languages)
34-
self.supported_dataset_names = set(sast.supported_dataset_names)
35-
else:
36-
self.supported_languages &= set(sast.supported_languages)
37-
self.supported_dataset_names &= set(sast.supported_dataset_names)
38-
39-
self.supported_datasets = [
40-
DATASETS_ALL[d] for d in self.supported_dataset_names
41-
]
42-
43-
@property
44-
def supported_dataset_full_names(self) -> set[str]:
45-
"""List all language-specific datasets supported by all enabled SAST tools."""
46-
datasets_full_name = set()
47-
for dataset in self.supported_datasets:
48-
for dataset_full_name in dataset.list_dataset_full_names():
49-
dataset_name, lang = dataset_full_name.split("_")
50-
if lang in self.supported_languages:
51-
datasets_full_name.add(dataset_full_name)
52-
return datasets_full_name
31+
for lang in sast.supported_languages:
32+
if self.sasts_by_lang.get(lang):
33+
self.sasts_by_lang[lang].append(sast)
34+
else:
35+
self.sasts_by_lang[lang] = [sast]
36+
37+
for dataset in sast.supported_datasets:
38+
if self.sasts_by_dataset.get(dataset):
39+
self.sasts_by_dataset[dataset].append(sast)
40+
else:
41+
self.sasts_by_dataset[dataset] = [sast]
5342

5443
def list_results(
5544
self, project: bool = False, dataset: bool = False, limit: int | None = None

codesectools/sasts/core/sast/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class SAST(ABC):
5353
properties (SASTProperties): The properties of the SAST tool.
5454
requirements (SASTRequirements): The requirements for the SAST tool.
5555
commands (list[list[str]]): Command-line templates to be executed.
56+
valid_codes (list[int]): A list of exit codes indicating that the command did not fail.
5657
environ (dict[str, str]): Environment variables to set for commands.
5758
output_files (list[tuple[Path, bool]]): Expected output files and
5859
whether they are required.
@@ -75,6 +76,7 @@ class SAST(ABC):
7576
properties: SASTProperties
7677
requirements: SASTRequirements
7778
commands: list[list[str]]
79+
valid_codes: list[int]
7880
environ: dict[str, str] = {}
7981
output_files: list[tuple[Path, bool]]
8082
parser: AnalysisResult
@@ -84,9 +86,7 @@ class SAST(ABC):
8486
def __init__(self) -> None:
8587
"""Initialize the SAST instance.
8688
87-
Set up the list of supported dataset objects based on the
88-
`supported_dataset_names` class attribute and define the tool-specific
89-
output directory.
89+
Set up supported datasets, the output directory, and requirement status.
9090
"""
9191
self.supported_datasets = [
9292
DATASETS_ALL[d] for d in self.supported_dataset_names
@@ -153,7 +153,7 @@ def run_analysis(
153153
rendered_command = self.render_command(command, render_variables)
154154
retcode, out = run_command(rendered_command, project_dir, self.environ)
155155
command_output += out
156-
if retcode != 0:
156+
if retcode not in self.valid_codes:
157157
raise NonZeroExit(rendered_command, command_output)
158158
end = time.time()
159159

codesectools/sasts/tools/Bearer/sast.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class BearerSAST(BuildlessSAST):
2727
properties (SASTProperties): The properties of the SAST tool.
2828
requirements (SASTRequirements): The requirements for the SAST tool.
2929
commands (list[list[str]]): A list of command-line templates to be executed.
30+
valid_codes (list[int]): A list of exit codes indicating that the command did not fail.
3031
output_files (list[tuple[Path, bool]]): A list of expected output files and
3132
whether they are required.
3233
parser (type[BearerAnalysisResult]): The parser class for the tool's results.
@@ -64,6 +65,7 @@ class BearerSAST(BuildlessSAST):
6465
"--exit-code=0",
6566
]
6667
]
68+
valid_codes = [0]
6769
output_files = [
6870
(Path("bearer_output.json"), True),
6971
]

codesectools/sasts/tools/Coverity/sast.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class CoveritySAST(BuildlessSAST):
2626
properties (SASTProperties): The properties of the SAST tool.
2727
requirements (SASTRequirements): The requirements for the SAST tool.
2828
commands (list[list[str]]): A list of command-line templates to be executed.
29+
valid_codes (list[int]): A list of exit codes indicating that the command did not fail.
2930
output_files (list[tuple[Path, bool]]): A list of expected output files and
3031
whether they are required.
3132
parser (type[CoverityAnalysisResult]): The parser class for the tool's results.
@@ -69,6 +70,7 @@ class CoveritySAST(BuildlessSAST):
6970
"--enable-callgraph-metrics",
7071
],
7172
]
73+
valid_codes = [0]
7274
output_files = [
7375
(Path("coverity.yaml"), False),
7476
(Path("idir", "coverity-cli", "capture-files-src-list*"), True),

codesectools/sasts/tools/SemgrepCE/sast.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class SemgrepCESAST(BuildlessSAST):
2727
properties (SASTProperties): The properties of the SAST tool.
2828
requirements (SASTRequirements): The requirements for the SAST tool.
2929
commands (list[list[str]]): A list of command-line templates to be executed.
30+
valid_codes (list[int]): A list of exit codes indicating that the command did not fail.
3031
output_files (list[tuple[Path, bool]]): A list of expected output files and
3132
whether they are required.
3233
parser (type[SemgrepCEAnalysisResult]): The parser class for the tool's results.
@@ -35,7 +36,7 @@ class SemgrepCESAST(BuildlessSAST):
3536
"""
3637

3738
name = "SemgrepCE"
38-
supported_languages = ["java"]
39+
supported_languages = ["java", "c"]
3940
supported_dataset_names = ["BenchmarkJava", "CVEfixes"]
4041
properties = SASTProperties(free=True, offline=True)
4142
requirements = SASTRequirements(
@@ -60,6 +61,7 @@ class SemgrepCESAST(BuildlessSAST):
6061
"--json-output=semgrepce_output.json",
6162
]
6263
]
64+
valid_codes = [0, 1] # https://semgrep.dev/docs/cli-reference#exit-codes
6365
output_files = [
6466
(Path("semgrepce_output.json"), True),
6567
]

codesectools/sasts/tools/SnykCode/parser.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class SnykCodeAnalysisResult(AnalysisResult):
5454
metadata, including timings, file lists, and defects.
5555
"""
5656

57+
normalize_lang_names = {"cpp": ["c", "cpp"]}
58+
5759
def __init__(self, output_dir: Path, result_data: dict, cmdout: dict) -> None:
5860
"""Initialize a SnykCodeAnalysisResult instance.
5961
@@ -81,7 +83,7 @@ def __init__(self, output_dir: Path, result_data: dict, cmdout: dict) -> None:
8183
for result in run["results"]:
8284
rule_index = result["ruleIndex"]
8385
lang, *_, checker = result["ruleId"].split("/")
84-
if lang != self.lang:
86+
if self.lang not in self.normalize_lang_names[lang]:
8587
continue
8688

8789
defect = SnykCodeIssue(

0 commit comments

Comments
 (0)