codeaudit 1.9.2__py3-none-any.whl → 1.9.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeaudit/__about__.py +1 -1
- codeaudit/altairplots.py +7 -29
- codeaudit/api_interfaces.py +87 -53
- codeaudit/api_reporting.py +23 -7
- codeaudit/benchmark_helpfunctions.py +325 -0
- codeaudit/data/sastchecks.csv +1 -1
- codeaudit/filehelpfunctions.py +27 -6
- codeaudit/pypi_package_scan.py +15 -18
- codeaudit/reporting.py +25 -8
- codeaudit/suppression.py +2 -28
- {codeaudit-1.9.2.dist-info → codeaudit-1.9.4.dist-info}/METADATA +4 -3
- {codeaudit-1.9.2.dist-info → codeaudit-1.9.4.dist-info}/RECORD +15 -14
- {codeaudit-1.9.2.dist-info → codeaudit-1.9.4.dist-info}/WHEEL +1 -1
- {codeaudit-1.9.2.dist-info → codeaudit-1.9.4.dist-info}/entry_points.txt +0 -0
- {codeaudit-1.9.2.dist-info → codeaudit-1.9.4.dist-info}/licenses/LICENSE.txt +0 -0
codeaudit/__about__.py
CHANGED
codeaudit/altairplots.py
CHANGED
|
@@ -18,7 +18,8 @@ import pandas as pd
|
|
|
18
18
|
|
|
19
19
|
from collections import Counter
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
|
|
21
|
+
from codeaudit.api_interfaces import get_weakness_counts
|
|
22
|
+
from codeaudit.api_reporting import total_weaknesses
|
|
22
23
|
|
|
23
24
|
def module_count_barchart(scanresult):
|
|
24
25
|
"""Create a bar chart showing module counts by category.
|
|
@@ -689,20 +690,8 @@ def weaknesses_overview(scanresult):
|
|
|
689
690
|
.mark_text()
|
|
690
691
|
.encode(text="msg:N")
|
|
691
692
|
)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
counter = Counter()
|
|
695
|
-
for file_info in file_security_info.values():
|
|
696
|
-
if not isinstance(file_info, dict):
|
|
697
|
-
continue
|
|
698
|
-
sast_result = file_info.get("sast_result")
|
|
699
|
-
if not isinstance(sast_result, dict):
|
|
700
|
-
continue
|
|
701
|
-
for finding in sast_result.values():
|
|
702
|
-
if isinstance(finding, dict):
|
|
703
|
-
validation = finding.get("validation")
|
|
704
|
-
if validation and isinstance(validation, str):
|
|
705
|
-
counter[validation] += 1
|
|
693
|
+
# --- Count every validation across all files ---
|
|
694
|
+
counter = Counter(get_weakness_counts(scanresult))
|
|
706
695
|
|
|
707
696
|
if not counter:
|
|
708
697
|
return (
|
|
@@ -875,7 +864,7 @@ def sast_files_overview(scanresult):
|
|
|
875
864
|
"base_name": base_name,
|
|
876
865
|
"parent_folder": parent_folder,
|
|
877
866
|
"full_path": str(filepath),
|
|
878
|
-
"issues":
|
|
867
|
+
"issues": total_weaknesses(scanresult),
|
|
879
868
|
"complexity": file_info.get("Complexity_Score", 0),
|
|
880
869
|
}
|
|
881
870
|
)
|
|
@@ -977,19 +966,8 @@ def weaknesses_radial_overview(scanresult):
|
|
|
977
966
|
)
|
|
978
967
|
|
|
979
968
|
# --- Count every 'validation' across all files ---
|
|
980
|
-
counter = Counter()
|
|
981
|
-
|
|
982
|
-
if not isinstance(file_info, dict):
|
|
983
|
-
continue
|
|
984
|
-
sast_result = file_info.get("sast_result")
|
|
985
|
-
if not isinstance(sast_result, dict):
|
|
986
|
-
continue
|
|
987
|
-
for finding in sast_result.values():
|
|
988
|
-
if isinstance(finding, dict):
|
|
989
|
-
validation = finding.get("validation")
|
|
990
|
-
if validation and isinstance(validation, str):
|
|
991
|
-
counter[validation] += 1
|
|
992
|
-
|
|
969
|
+
counter = Counter(get_weakness_counts(scanresult))
|
|
970
|
+
|
|
993
971
|
if not counter:
|
|
994
972
|
return (
|
|
995
973
|
alt.Chart(
|
codeaudit/api_interfaces.py
CHANGED
|
@@ -138,8 +138,8 @@ def filescan(input_path, nosec=False):
|
|
|
138
138
|
output |= {"package_name": package_name}
|
|
139
139
|
scan_output = _codeaudit_directory_scan(input_path, nosec_flag=nosec)
|
|
140
140
|
output |= scan_output
|
|
141
|
-
return output
|
|
142
|
-
elif is_ast_parsable(input_path):
|
|
141
|
+
return output
|
|
142
|
+
elif file_path.name.endswith(".py") and is_ast_parsable(input_path): # check on parsable single Python file
|
|
143
143
|
# do a file check
|
|
144
144
|
file_information = overview_per_file(input_path)
|
|
145
145
|
module_information = get_modules(input_path) # modules per file
|
|
@@ -312,41 +312,68 @@ def read_input_file(filename, safe_directory="data_folder"):
|
|
|
312
312
|
except json.JSONDecodeError as e:
|
|
313
313
|
raise json.JSONDecodeError(f"Invalid JSON in file: {file_path}", e.doc, e.pos)
|
|
314
314
|
|
|
315
|
-
|
|
316
315
|
def get_weakness_counts(input_file, nosec=False):
|
|
317
|
-
"""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
"""Count SAST weaknesses from a scan result or by scanning a file/package.
|
|
317
|
+
|
|
318
|
+
Accepts either a pre-computed scan result dictionary or a path to a file
|
|
319
|
+
or package. In the latter case the path is scanned with `filescan`.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
input_file: Path to a file/package to scan, or an existing scan result
|
|
323
|
+
dictionary.
|
|
324
|
+
nosec: Whether to pass the `nosec` flag to `filescan`. Defaults to False.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
A dictionary mapping each weakness type (str) to its total occurrence
|
|
328
|
+
count (int) across all scanned files. Returns an empty dict if no
|
|
329
|
+
valid security information is present.
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
ValueError: If the scan result is invalid or contains an error.
|
|
333
|
+
"""
|
|
334
|
+
# Accept an existing scan result or scan a file/package
|
|
335
|
+
if isinstance(input_file, dict):
|
|
336
|
+
scan_result = input_file
|
|
337
|
+
else:
|
|
338
|
+
scan_result = filescan(input_file, nosec)
|
|
322
339
|
|
|
323
340
|
if not isinstance(scan_result, dict):
|
|
324
|
-
raise ValueError("
|
|
341
|
+
raise ValueError("Invalid scan result")
|
|
325
342
|
|
|
326
343
|
if "Error" in scan_result:
|
|
327
344
|
raise ValueError(scan_result["Error"])
|
|
328
345
|
|
|
329
346
|
file_security_info = scan_result.get("file_security_info")
|
|
347
|
+
|
|
330
348
|
if not isinstance(file_security_info, dict):
|
|
331
349
|
return {}
|
|
332
350
|
|
|
333
351
|
counter = Counter()
|
|
334
352
|
|
|
335
353
|
for file_info in file_security_info.values():
|
|
354
|
+
|
|
336
355
|
if not isinstance(file_info, dict):
|
|
337
356
|
continue
|
|
338
357
|
|
|
339
358
|
sast_result = file_info.get("sast_result", {})
|
|
359
|
+
|
|
340
360
|
if not isinstance(sast_result, dict):
|
|
341
361
|
continue
|
|
342
362
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if isinstance(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
363
|
+
for finding in sast_result.values():
|
|
364
|
+
|
|
365
|
+
# Support individual findings and lists of findings
|
|
366
|
+
findings = finding if isinstance(finding, list) else [finding]
|
|
367
|
+
|
|
368
|
+
for issue in findings:
|
|
369
|
+
|
|
370
|
+
if not isinstance(issue, dict):
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
weakness_type = issue.get("validation", "Unknown")
|
|
374
|
+
|
|
375
|
+
if isinstance(weakness_type, str) and weakness_type:
|
|
376
|
+
counter[weakness_type] += 1
|
|
350
377
|
|
|
351
378
|
return dict(counter)
|
|
352
379
|
|
|
@@ -511,6 +538,50 @@ def get_default_validations():
|
|
|
511
538
|
return output
|
|
512
539
|
|
|
513
540
|
|
|
541
|
+
def get_weakness_counts_per_file(scanresult):
|
|
542
|
+
"""Creates an overview of the SAST scan: Number of weaknesses per file.
|
|
543
|
+
|
|
544
|
+
Aggregates SAST findings
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
scanresult (dict): Scan result containing "file_security_info"
|
|
548
|
+
with per-file SAST findings and metadata.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
result (dict): Just number of weaknesses per file scanned
|
|
552
|
+
"""
|
|
553
|
+
file_security_info = scanresult.get("file_security_info")
|
|
554
|
+
if not isinstance(file_security_info, dict) or not file_security_info:
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
records = []
|
|
558
|
+
for file_info in file_security_info.values():
|
|
559
|
+
if not isinstance(file_info, dict):
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
sast_result = file_info.get("sast_result")
|
|
563
|
+
if not isinstance(sast_result, dict):
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
filepath = file_info.get("FilePath") or file_info.get("file_name", "")
|
|
567
|
+
path_obj = Path(str(filepath))
|
|
568
|
+
|
|
569
|
+
base_name = file_info.get("FileName") or path_obj.name or "Unknown"
|
|
570
|
+
parent_folder = path_obj.parent.name if len(path_obj.parts) > 1 else None
|
|
571
|
+
if parent_folder in ("", ".", "/"):
|
|
572
|
+
parent_folder = None
|
|
573
|
+
|
|
574
|
+
records.append(
|
|
575
|
+
{
|
|
576
|
+
"base_name": base_name,
|
|
577
|
+
"issues": len(sast_result),
|
|
578
|
+
}
|
|
579
|
+
)
|
|
580
|
+
if not records:
|
|
581
|
+
return None
|
|
582
|
+
return records
|
|
583
|
+
|
|
584
|
+
|
|
514
585
|
def _generation_info():
|
|
515
586
|
"""Internal function to retrieve generation info for APIs output"""
|
|
516
587
|
ca_version_info = {"name": "Python_Code_Audit", "version": __version__}
|
|
@@ -638,43 +709,6 @@ def egress_check(input_path):
|
|
|
638
709
|
return output
|
|
639
710
|
|
|
640
711
|
|
|
641
|
-
def get_construct_counts(input_file):
|
|
642
|
-
"""
|
|
643
|
-
Analyze a Python file or package(directory) and count occurrences of code constructs (aka weaknesses).
|
|
644
|
-
|
|
645
|
-
This function uses `filescan` API call to retrieve security-related information
|
|
646
|
-
about the input file. This returns a dict. Then it counts how many times each code construct
|
|
647
|
-
appears across all scanned files.
|
|
648
|
-
|
|
649
|
-
Args:
|
|
650
|
-
input_file (str): Path to the file or directory(package) to scan.
|
|
651
|
-
|
|
652
|
-
Returns:
|
|
653
|
-
dict: A dictionary mapping each construct name (str) to the total
|
|
654
|
-
number of occurrences (int) across all scanned files.
|
|
655
|
-
|
|
656
|
-
Notes:
|
|
657
|
-
- The `filescan` function is expected to return a dictionary with
|
|
658
|
-
a 'file_security_info' key, containing per-file information.
|
|
659
|
-
- Each file's 'sast_result' should be a dictionary mapping
|
|
660
|
-
construct names to lists of occurrences.
|
|
661
|
-
"""
|
|
662
|
-
scan_result = filescan(input_file)
|
|
663
|
-
counter = Counter()
|
|
664
|
-
|
|
665
|
-
for file_info in scan_result.get("file_security_info", {}).values():
|
|
666
|
-
sast_result = file_info.get("sast_result", {})
|
|
667
|
-
for (
|
|
668
|
-
construct,
|
|
669
|
-
occurence,
|
|
670
|
-
) in (
|
|
671
|
-
sast_result.items()
|
|
672
|
-
): # occurence is times the construct appears in a single file
|
|
673
|
-
counter[construct] += len(occurence)
|
|
674
|
-
|
|
675
|
-
return dict(counter)
|
|
676
|
-
|
|
677
|
-
|
|
678
712
|
def _build_weakness_details(sastresult, filename_location):
|
|
679
713
|
"""
|
|
680
714
|
Builds a mapping of line numbers to SAST issue details.
|
codeaudit/api_reporting.py
CHANGED
|
@@ -19,17 +19,33 @@ So all results are returned as Pandas Dataframe. This makes things easier for fu
|
|
|
19
19
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
def total_weaknesses(scan_result):
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
"""Count all individual weaknesses in a scan result.
|
|
24
|
+
|
|
25
|
+
Walks through each file's SAST findings and adds up every
|
|
26
|
+
individual weakness found.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
scan_result (dict): Scan output with a "file_security_info" key
|
|
30
|
+
mapping file names to their per-file results. Each file's
|
|
31
|
+
"sast_result" maps check names to either a single finding
|
|
32
|
+
or a list of findings.
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
Returns:
|
|
35
|
+
int: Total number of individual weaknesses across all files.
|
|
36
|
+
"""
|
|
29
37
|
total = 0
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
file_security_info = scan_result.get("file_security_info", {})
|
|
40
|
+
|
|
41
|
+
for file_info in file_security_info.values():
|
|
32
42
|
sast_result = file_info.get("sast_result", {})
|
|
33
|
-
|
|
43
|
+
|
|
44
|
+
for finding in sast_result.values():
|
|
45
|
+
if isinstance(finding, list):
|
|
46
|
+
total += len(finding)
|
|
47
|
+
else:
|
|
48
|
+
total += 1
|
|
34
49
|
|
|
35
50
|
return total
|
|
51
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
License GPLv3 or higher.
|
|
3
|
+
|
|
4
|
+
(C) 2026 Created by Maikel Mardjan - https://nocomplexity.com/
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
9
|
+
|
|
10
|
+
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
11
|
+
|
|
12
|
+
API functions: Helpers for SAST Benchmark suite
|
|
13
|
+
"""
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import re
|
|
16
|
+
import ast
|
|
17
|
+
import pandas as pd
|
|
18
|
+
import panel as pn
|
|
19
|
+
from codeaudit.api_interfaces import filescan , get_weakness_counts_per_file
|
|
20
|
+
from codeaudit.filehelpfunctions import collect_python_source_files
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_project_root():
|
|
24
|
+
current = Path(__file__).resolve()
|
|
25
|
+
# Walk up the directory tree looking for project markers
|
|
26
|
+
for parent in [current, *current.parents]:
|
|
27
|
+
if (parent / "pyproject.toml").exists() and (parent / "tests").exists():
|
|
28
|
+
return parent
|
|
29
|
+
raise FileNotFoundError("Could not find project root.")
|
|
30
|
+
|
|
31
|
+
# Build the path dynamically
|
|
32
|
+
PROJECT_ROOT = get_project_root()
|
|
33
|
+
BENCHMARK_FILES = PROJECT_ROOT / "tests" / "benchmarkfiles" / "validpython"
|
|
34
|
+
|
|
35
|
+
import ast
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_sast_meta(source):
|
|
40
|
+
"""
|
|
41
|
+
Extract the top-level SAST_META dictionary from a Python file or source string.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
source: A file path (str or Path) or Python source code string.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The SAST_META value as a Python dict, or None if not found / invalid.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(source, (str, Path)):
|
|
50
|
+
try:
|
|
51
|
+
path = Path(source)
|
|
52
|
+
except (ValueError, TypeError) as exc:
|
|
53
|
+
# Input cannot be a path (e.g. null byte in string, wrong type).
|
|
54
|
+
# This is expected for source-code strings, so stay quiet-ish,
|
|
55
|
+
# but still report the reason instead of swallowing it.
|
|
56
|
+
print(f"extract_sast_meta: not a valid path ({type(exc).__name__}: {exc})")
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
if path.is_file():
|
|
60
|
+
source = path.read_text(encoding="utf-8")
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
# e.g. embedded null byte in the path.
|
|
63
|
+
print(
|
|
64
|
+
f"extract_sast_meta: invalid path "
|
|
65
|
+
f"({type(exc).__name__}: {exc}); treating as source"
|
|
66
|
+
)
|
|
67
|
+
except OSError as exc:
|
|
68
|
+
# Permissions, broken symlink, I/O error, etc.
|
|
69
|
+
print(
|
|
70
|
+
f"extract_sast_meta: could not read file "
|
|
71
|
+
f"({type(exc).__name__}: {exc.strerror or 'I/O error'}); "
|
|
72
|
+
f"falling back to treating input as source"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
tree = ast.parse(source)
|
|
77
|
+
except (SyntaxError, ValueError):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
for node in tree.body:
|
|
81
|
+
value_node = None
|
|
82
|
+
|
|
83
|
+
# Normal assignment: SAST_META = {...}
|
|
84
|
+
if isinstance(node, ast.Assign):
|
|
85
|
+
for target in node.targets:
|
|
86
|
+
if isinstance(target, ast.Name) and target.id == "SAST_META":
|
|
87
|
+
value_node = node.value
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
# Annotated assignment: SAST_META: dict = {...}
|
|
91
|
+
elif isinstance(node, ast.AnnAssign):
|
|
92
|
+
if (
|
|
93
|
+
isinstance(node.target, ast.Name)
|
|
94
|
+
and node.target.id == "SAST_META"
|
|
95
|
+
and node.value is not None
|
|
96
|
+
):
|
|
97
|
+
value_node = node.value
|
|
98
|
+
|
|
99
|
+
if value_node is not None:
|
|
100
|
+
try:
|
|
101
|
+
data = ast.literal_eval(value_node)
|
|
102
|
+
except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
|
|
103
|
+
return None
|
|
104
|
+
return data if isinstance(data, dict) else None
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_sast_meta_overview(directory):
|
|
110
|
+
"""Creates a list which contains the SAST_META data of all files in a directory
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
list[dict]: A list of dictionaries, one per Python source file found.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
files_to_check = collect_python_source_files(directory)
|
|
117
|
+
all_sast_info = []
|
|
118
|
+
file_info = {}
|
|
119
|
+
for file in files_to_check:
|
|
120
|
+
file_name = Path(file).name
|
|
121
|
+
file_info = { "filename" :file_name } | extract_sast_meta(file)
|
|
122
|
+
all_sast_info.append(file_info)
|
|
123
|
+
return all_sast_info
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_issues_with_titles(issues_data, titles_data):
|
|
127
|
+
"""
|
|
128
|
+
Create a list of dicts containing filename, weaknesses, and title.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
issues_data: list of dicts with 'base_name' and 'weaknesses'
|
|
132
|
+
titles_data: list of dicts with 'filename' and 'title' (plus other fields)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
list of dicts: [{'filename': ..., 'weaknesses': ..., 'title': ...}, ...]
|
|
136
|
+
"""
|
|
137
|
+
# Build a lookup from filename -> title
|
|
138
|
+
title_lookup = {item['filename']: item['title'] for item in titles_data}
|
|
139
|
+
|
|
140
|
+
result = []
|
|
141
|
+
for item in issues_data:
|
|
142
|
+
filename = item['base_name']
|
|
143
|
+
result.append({
|
|
144
|
+
'filename': filename,
|
|
145
|
+
'title': title_lookup.get(filename) , # None if no matching title found
|
|
146
|
+
'weaknesses': item['issues'],
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
def benchmark_overview_issues_per_file(benchmark_files=BENCHMARK_FILES):
|
|
152
|
+
"""Creates a overview of weaknesses per benchmark file"""
|
|
153
|
+
benchmark_results = filescan(benchmark_files)
|
|
154
|
+
benchmark_list = get_weakness_counts_per_file(benchmark_results)
|
|
155
|
+
titles = create_sast_meta_overview(benchmark_files)
|
|
156
|
+
overview = create_issues_with_titles(benchmark_list , titles)
|
|
157
|
+
df = pd.DataFrame(overview)
|
|
158
|
+
df = df.sort_values(by='filename').reset_index(drop=True)
|
|
159
|
+
formatters = {
|
|
160
|
+
"title": {"type": "textarea"},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
table = pn.widgets.Tabulator(
|
|
164
|
+
df,
|
|
165
|
+
header_filters=False,
|
|
166
|
+
show_index=False,
|
|
167
|
+
formatters=formatters,
|
|
168
|
+
pagination="local",
|
|
169
|
+
page_size=50,
|
|
170
|
+
layout='fit_columns',
|
|
171
|
+
selectable=True,
|
|
172
|
+
theme="semantic-ui",
|
|
173
|
+
# --- fixed column width ---
|
|
174
|
+
widths={'weaknesses': 150 ,
|
|
175
|
+
'title': 350,
|
|
176
|
+
'filename': 200}, # pixels
|
|
177
|
+
)
|
|
178
|
+
return table
|
|
179
|
+
|
|
180
|
+
def cwe_to_html(cwe_value, column_name="cwe_id"):
|
|
181
|
+
"""
|
|
182
|
+
Convert a CWE value (single int, or list of ints) into an HTML clickable string.
|
|
183
|
+
Each CWE number N becomes <a href="https://cwe.mitre.org/data/definitions/N.html">N</a>.
|
|
184
|
+
Lists are rendered as comma-separated clickable links.
|
|
185
|
+
Empty / NaN values are returned unchanged (as empty string).
|
|
186
|
+
"""
|
|
187
|
+
if cwe_value is None:
|
|
188
|
+
return ""
|
|
189
|
+
|
|
190
|
+
# Handle NaN (float) safely. pd.isna raises TypeError/ValueError for
|
|
191
|
+
# array-like inputs (lists, tuples, sets), which we handle explicitly
|
|
192
|
+
# below — so those are expected. Anything else is reported rather than
|
|
193
|
+
# silently swallowed.
|
|
194
|
+
try:
|
|
195
|
+
if pd.isna(cwe_value):
|
|
196
|
+
return ""
|
|
197
|
+
except (TypeError, ValueError) as exc:
|
|
198
|
+
# Only report if the input isn't one of the shapes we already know
|
|
199
|
+
# how to handle; otherwise list inputs would print on every row.
|
|
200
|
+
if not isinstance(cwe_value, (list, tuple, set)):
|
|
201
|
+
print(
|
|
202
|
+
f"cwe_to_html[{column_name}]: pd.isna() failed unexpectedly "
|
|
203
|
+
f"({type(exc).__name__}: {exc})"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def make_link(n):
|
|
207
|
+
n = str(n).strip()
|
|
208
|
+
if not n.isdigit():
|
|
209
|
+
print(
|
|
210
|
+
f"cwe_to_html[{column_name}]: refusing non-numeric CWE value {n!r}"
|
|
211
|
+
)
|
|
212
|
+
return ""
|
|
213
|
+
return (
|
|
214
|
+
f'<a href="https://cwe.mitre.org/data/definitions/{n}.html" '
|
|
215
|
+
f'target="_blank">{n}</a>'
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# List / tuple / set of CWEs
|
|
219
|
+
if isinstance(cwe_value, (list, tuple, set)):
|
|
220
|
+
return ", ".join(make_link(n) for n in cwe_value)
|
|
221
|
+
|
|
222
|
+
# Single value (int, numpy int, or string)
|
|
223
|
+
return make_link(cwe_value)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def owasp_to_html(value):
|
|
228
|
+
"""
|
|
229
|
+
Convert an OWASP Top 10 string like "A04:2021 - Insecure Design"
|
|
230
|
+
or "A02:2025 - Security Misconfiguration" into an HTML clickable link.
|
|
231
|
+
|
|
232
|
+
URL pattern:
|
|
233
|
+
https://top10.owasp.org/{year}/{Axx}_{year}-{Name_With_Underscores}/
|
|
234
|
+
"""
|
|
235
|
+
if value is None:
|
|
236
|
+
return ""
|
|
237
|
+
try:
|
|
238
|
+
if pd.isna(value):
|
|
239
|
+
return ""
|
|
240
|
+
except (TypeError, ValueError) as e:
|
|
241
|
+
print(f"Warning: Could not evaluate pd.isna() on value due to {e}. Continuing.")
|
|
242
|
+
|
|
243
|
+
if value is None:
|
|
244
|
+
return ""
|
|
245
|
+
text = str(value).strip()
|
|
246
|
+
|
|
247
|
+
# Match "A04:2021 - Insecure Design" / "A02:2025 - Security Misconfiguration"
|
|
248
|
+
match = re.match(
|
|
249
|
+
r"^\s*(A\d{2})\s*:\s*(\d{4})\s*[-–—]\s*(.+?)\s*$",
|
|
250
|
+
text,
|
|
251
|
+
flags=re.IGNORECASE,
|
|
252
|
+
)
|
|
253
|
+
if not match:
|
|
254
|
+
# Return original text unchanged if it doesn't match the expected format
|
|
255
|
+
return text
|
|
256
|
+
|
|
257
|
+
category, year, name = match.groups()
|
|
258
|
+
category = category.upper()
|
|
259
|
+
year = year.strip()
|
|
260
|
+
name = name.strip()
|
|
261
|
+
|
|
262
|
+
# Convert "Security Misconfiguration" -> "Security_Misconfiguration"
|
|
263
|
+
# Collapse whitespace to single underscores
|
|
264
|
+
name_slug = re.sub(r"\s+", "_", name)
|
|
265
|
+
|
|
266
|
+
url = f"https://top10.owasp.org/{year}/{category}_{year}-{name_slug}/"
|
|
267
|
+
|
|
268
|
+
return f'<a href="{url}" target="_blank">{text}</a>'
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def transform_owasp_column(df, column="owasp", inplace=False):
|
|
272
|
+
"""
|
|
273
|
+
Apply owasp_to_html to every value in the specified column.
|
|
274
|
+
"""
|
|
275
|
+
if not inplace:
|
|
276
|
+
df = df.copy()
|
|
277
|
+
df[column] = df[column].apply(owasp_to_html)
|
|
278
|
+
return df
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def transform_cwe_column(df, column="cwe_id", inplace=False):
|
|
282
|
+
"""
|
|
283
|
+
Apply cwe_to_html to every value in the given column of a DataFrame.
|
|
284
|
+
"""
|
|
285
|
+
if not inplace:
|
|
286
|
+
df = df.copy()
|
|
287
|
+
df[column] = df[column].apply(cwe_to_html)
|
|
288
|
+
return df
|
|
289
|
+
|
|
290
|
+
def benchmark_suite_overview(benchmarklist=BENCHMARK_FILES):
|
|
291
|
+
"""Creates a Tabulator to display the benchmark suite collection of Python files
|
|
292
|
+
These files contain SAST meta information
|
|
293
|
+
|
|
294
|
+
input: benchmarklist , type list
|
|
295
|
+
"""
|
|
296
|
+
benchmarklist = create_sast_meta_overview(BENCHMARK_FILES)
|
|
297
|
+
df = pd.DataFrame(benchmarklist)
|
|
298
|
+
|
|
299
|
+
df = transform_cwe_column(df)
|
|
300
|
+
df = transform_owasp_column(df) #transform owasp column
|
|
301
|
+
df = df[['filename', 'title', 'cwe_id', 'weakness', 'owasp', 'severity']].copy()
|
|
302
|
+
|
|
303
|
+
formatters = {
|
|
304
|
+
"cwe_id": {"type": "html", "field": "html"},
|
|
305
|
+
"owasp": {"type": "html", "field": "html"},
|
|
306
|
+
"title": {"type": "textarea"},
|
|
307
|
+
"weakness": {"type": "textarea"},
|
|
308
|
+
"tags": {"type": "textarea"},
|
|
309
|
+
}
|
|
310
|
+
table = pn.widgets.Tabulator(
|
|
311
|
+
df,
|
|
312
|
+
header_filters=False,
|
|
313
|
+
show_index=False,
|
|
314
|
+
formatters=formatters,
|
|
315
|
+
pagination="local",
|
|
316
|
+
page_size=50,
|
|
317
|
+
layout='fit_columns',
|
|
318
|
+
selectable=True,
|
|
319
|
+
theme="simple",
|
|
320
|
+
sizing_mode='stretch_width', # Add this line
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return table
|
|
324
|
+
|
|
325
|
+
|
codeaudit/data/sastchecks.csv
CHANGED
|
@@ -9,7 +9,7 @@ Directory Creation,os.mknod,Low,Direct file system calls require careful input v
|
|
|
9
9
|
Directory Creation,os.makedev,Low,Direct file system calls require careful input validation to prevent vulnerabilities.
|
|
10
10
|
OS Forking,os.fork,Low,"On macOS use of this function is unsafe when mixed with using higher-level system APIs, and that includes using urllib.request."
|
|
11
11
|
Dangerous Built-in: eval,eval,High,This function can execute arbitrary code. Never safe with untrusted input.
|
|
12
|
-
Input Function,input,
|
|
12
|
+
Input Function,input,High,User input must be strictly sanitized and validated to prevent injection vulnerabilities.
|
|
13
13
|
Overly Broad Exception Handling,pass,Low,Using `pass` in an `except` block can silently ignore critical security exceptions.
|
|
14
14
|
Overly Broad Exception Handling,continue,Low,Skipping over exceptions can mask critical errors and security risks.
|
|
15
15
|
Dangerous Built-in: exec,exec,High,This function can execute arbitrary code and should be used only with validated constructs.
|
codeaudit/filehelpfunctions.py
CHANGED
|
@@ -71,7 +71,7 @@ def collect_python_source_files(directory):
|
|
|
71
71
|
full_path = os.path.join(root, file)
|
|
72
72
|
if os.path.isfile(full_path):
|
|
73
73
|
python_files.append(os.path.abspath(full_path))
|
|
74
|
-
# check if the file can be parsed using the AST
|
|
74
|
+
# check if the found python file can be parsed using the AST - so extra security check to keep SAST scanning highly secure!
|
|
75
75
|
final_file_list = []
|
|
76
76
|
for python_file in python_files:
|
|
77
77
|
if is_ast_parsable(python_file):
|
|
@@ -98,17 +98,38 @@ def get_filename_from_path(file_path):
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def is_ast_parsable(file_path):
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
Uses the
|
|
101
|
+
"""Check whether a Python source file passes validation.
|
|
102
|
+
|
|
103
|
+
Uses the security-focused Python validation functionality provided by
|
|
104
|
+
the `fileaudit` module to check whether the file contains valid,
|
|
105
|
+
parsable Python code.
|
|
106
|
+
|
|
107
|
+
Note:
|
|
108
|
+
This function is intended for Python source files only.
|
|
109
|
+
Passing a non-Python file may raise an exception. Ensure that
|
|
110
|
+
the file has a `.py` extension before calling this function.
|
|
104
111
|
|
|
105
112
|
Args:
|
|
106
113
|
file_path (str | Path): Path to the Python source file.
|
|
107
114
|
|
|
108
115
|
Returns:
|
|
109
|
-
bool: True if the file passes
|
|
116
|
+
bool: True if the Python file passes validation
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
Exception: If check fails. The underlying validation functionality raises
|
|
120
|
+
an exception, for example, when processing an unsupported
|
|
121
|
+
file type or invalid Python file
|
|
122
|
+
|
|
123
|
+
References:
|
|
124
|
+
https://github.com/nocomplexity/fileaudit
|
|
110
125
|
"""
|
|
111
|
-
|
|
126
|
+
try:
|
|
127
|
+
return bool(validate_python(Path(file_path)))
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f'Python security check failed for file:{file_path}: {e}') # Exception should not be passed silently.
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
112
133
|
|
|
113
134
|
|
|
114
135
|
def has_python_files(input_path):
|
codeaudit/pypi_package_scan.py
CHANGED
|
@@ -24,7 +24,7 @@ from urllib.error import HTTPError, URLError
|
|
|
24
24
|
from urllib.request import Request, urlopen
|
|
25
25
|
|
|
26
26
|
from datetime import date, datetime
|
|
27
|
-
from fileaudit.targz_check import validate_tar_gz
|
|
27
|
+
from fileaudit.targz_check import validate_tar_gz , TarValidationError
|
|
28
28
|
|
|
29
29
|
from codeaudit import __version__
|
|
30
30
|
|
|
@@ -94,26 +94,23 @@ def is_valid_tar_gz(path):
|
|
|
94
94
|
try:
|
|
95
95
|
path = os.fspath(path)
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
path
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
max_file_size=15 * 1024 * 1024,
|
|
102
|
-
max_tar_members=12000,
|
|
103
|
-
)
|
|
104
|
-
and tarfile.is_tarfile(path)
|
|
97
|
+
validate_tar_gz(
|
|
98
|
+
path,
|
|
99
|
+
max_file_size=15 * 1024 * 1024,
|
|
100
|
+
max_tar_members=12000,
|
|
105
101
|
)
|
|
106
|
-
except (OSError, ValueError, TypeError, AttributeError, tarfile.TarError):
|
|
107
|
-
return False
|
|
108
102
|
|
|
103
|
+
return tarfile.is_tarfile(path)
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
except (
|
|
106
|
+
OSError,
|
|
107
|
+
ValueError,
|
|
108
|
+
TypeError,
|
|
109
|
+
AttributeError,
|
|
110
|
+
tarfile.TarError,
|
|
111
|
+
TarValidationError,
|
|
112
|
+
):
|
|
113
|
+
return False
|
|
117
114
|
|
|
118
115
|
|
|
119
116
|
def get_targz_package_source(
|
codeaudit/reporting.py
CHANGED
|
@@ -22,7 +22,7 @@ import pandas as pd
|
|
|
22
22
|
import html
|
|
23
23
|
import datetime
|
|
24
24
|
from importlib.resources import files
|
|
25
|
-
from fileaudit.targz_check import validate_tar_gz
|
|
25
|
+
from fileaudit.targz_check import validate_tar_gz , TarValidationError
|
|
26
26
|
|
|
27
27
|
from codeaudit import __version__
|
|
28
28
|
|
|
@@ -79,6 +79,24 @@ SIMPLE_CSS_FILE = files("codeaudit") / "simple.css"
|
|
|
79
79
|
DEFAULT_OUTPUT_FILE = "codeaudit-report.html"
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def safe_validate_tar_gz(directory, **kwargs):
|
|
83
|
+
"""Safely validates a TAR.GZ archive.
|
|
84
|
+
|
|
85
|
+
Returns False if validation raises an exception.
|
|
86
|
+
validate_tar_gz gives True or raises an exception is the check fails. See fileaudit apidocs.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
directory: Path to the TAR.GZ archive.
|
|
90
|
+
**kwargs: Additional keyword arguments passed to validate_tar_gz().
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
bool: True if validation succeeds, False otherwise.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
return validate_tar_gz(directory, **kwargs)
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
82
100
|
def overview_report(directory, filename=DEFAULT_OUTPUT_FILE):
|
|
83
101
|
"""Generate an overview report of code complexity and security indicators.
|
|
84
102
|
|
|
@@ -159,9 +177,9 @@ def overview_report(directory, filename=DEFAULT_OUTPUT_FILE):
|
|
|
159
177
|
if not has_python_files(directory):
|
|
160
178
|
print(f"ERROR: Directory '{directory}' contains no Python files.")
|
|
161
179
|
exit(1)
|
|
162
|
-
elif file_path.name.endswith(".tar.gz") and
|
|
180
|
+
elif file_path.name.endswith(".tar.gz") and safe_validate_tar_gz(
|
|
163
181
|
directory, max_file_size=15 * 1024 * 1024, max_tar_members=12000
|
|
164
|
-
): # for local tar.gz or remote tar.gz scans
|
|
182
|
+
): # for local tar.gz or remote tar.gz scans - mind checking extension first MUST be done!
|
|
165
183
|
package_name = directory
|
|
166
184
|
advice = f'<p>👉 To perform a SAST scan on the source code, run:<pre><code class="language-python">codeaudit filescan {directory}</code></pre></p>'
|
|
167
185
|
release = "N.A."
|
|
@@ -378,8 +396,8 @@ def scan_report(input_path, filename=DEFAULT_OUTPUT_FILE, nosec=False):
|
|
|
378
396
|
if file_path.is_dir():
|
|
379
397
|
directory_scan_report(
|
|
380
398
|
input_path, nosec_flag=nosec, filename=filename
|
|
381
|
-
) # create a package aka directory scan report
|
|
382
|
-
elif is_ast_parsable(input_path):
|
|
399
|
+
) # create a package aka directory scan report
|
|
400
|
+
elif file_path.name.endswith(".py") and is_ast_parsable(input_path):
|
|
383
401
|
# create a sast file check report
|
|
384
402
|
if not nosec: # no filtering on reviewed items with markers in code
|
|
385
403
|
scan_output = perform_validations(
|
|
@@ -404,10 +422,9 @@ def scan_report(input_path, filename=DEFAULT_OUTPUT_FILE, nosec=False):
|
|
|
404
422
|
html_output += "<br>"
|
|
405
423
|
html_output += DISCLAIMER_TEXT
|
|
406
424
|
create_htmlfile(html_output, filename)
|
|
407
|
-
|
|
408
|
-
elif file_path.name.endswith(".tar.gz") and validate_tar_gz(
|
|
425
|
+
elif file_path.name.endswith(".tar.gz") and safe_validate_tar_gz(
|
|
409
426
|
input_path, max_file_size=15 * 1024 * 1024, max_tar_members=12000
|
|
410
|
-
): # for local tar.gz or remote tar.gz scans
|
|
427
|
+
): # for local tar.gz or remote tar.gz scans - mind checking extension first MUST be done!
|
|
411
428
|
src_dir, tmp_handle = get_targz_package_source(input_path)
|
|
412
429
|
directory_scan_report(
|
|
413
430
|
src_dir,
|
codeaudit/suppression.py
CHANGED
|
@@ -117,33 +117,6 @@ def get_start_to_end_lines(filename):
|
|
|
117
117
|
|
|
118
118
|
return end_lines
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
# def get_start_to_end_lines(filename):
|
|
122
|
-
# """
|
|
123
|
-
# Parse AST once and build mapping: start_line → highest end_line found for nodes
|
|
124
|
-
# starting on that line.
|
|
125
|
-
# """
|
|
126
|
-
# end_lines = {}
|
|
127
|
-
|
|
128
|
-
# try:
|
|
129
|
-
# with open(filename, 'r', encoding='utf-8') as f:
|
|
130
|
-
# source = f.read()
|
|
131
|
-
# tree = ast.parse(source)
|
|
132
|
-
|
|
133
|
-
# for node in ast.walk(tree):
|
|
134
|
-
# if not hasattr(node, 'lineno'):
|
|
135
|
-
# continue
|
|
136
|
-
# start = node.lineno
|
|
137
|
-
# end = getattr(node, 'end_lineno', start)
|
|
138
|
-
# # Take the maximum end line if multiple nodes start on same line
|
|
139
|
-
# if start not in end_lines or end > end_lines[start]:
|
|
140
|
-
# end_lines[start] = end
|
|
141
|
-
# except Exception:
|
|
142
|
-
# pass
|
|
143
|
-
|
|
144
|
-
# return end_lines
|
|
145
|
-
|
|
146
|
-
|
|
147
120
|
def is_suppressed(line, comments_by_line, start_to_end, match_func):
|
|
148
121
|
"""
|
|
149
122
|
Check if the statement starting at `line` is suppressed by looking at comments
|
|
@@ -225,6 +198,7 @@ def match_suppression_keyword(comment_line):
|
|
|
225
198
|
"false-positive",
|
|
226
199
|
"falsepositive",
|
|
227
200
|
"risk-accepted",
|
|
201
|
+
"ruff:ignore",
|
|
228
202
|
"security-accepted",
|
|
229
203
|
"security-reviewed",
|
|
230
204
|
"security-exception",
|
|
@@ -234,5 +208,5 @@ def match_suppression_keyword(comment_line):
|
|
|
234
208
|
return False
|
|
235
209
|
|
|
236
210
|
normalized = " ".join(word.lstrip("#").lower() for word in comment_line.split())
|
|
237
|
-
tokens = re.split(r"[^\w
|
|
211
|
+
tokens = re.split(r"[^\w\-:]+", normalized)
|
|
238
212
|
return any(marker.lower() in tokens for marker in MARKER_LIST)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codeaudit
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.4
|
|
4
4
|
Summary: A modern Python security source code analyzer (SAST) based on distrust.
|
|
5
5
|
Project-URL: Homepage, https://codeaudit.nocomplexity.com
|
|
6
6
|
Project-URL: Documentation, https://github.com/nocomplexity/codeaudit#readme
|
|
@@ -18,11 +18,12 @@ Classifier: Programming Language :: Python
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
22
|
Classifier: Topic :: Security
|
|
22
23
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
24
|
Requires-Python: >=3.12
|
|
24
25
|
Requires-Dist: altair==6.0.0
|
|
25
|
-
Requires-Dist: fileaudit
|
|
26
|
+
Requires-Dist: fileaudit==0.2.5
|
|
26
27
|
Requires-Dist: fire==0.7.1
|
|
27
28
|
Requires-Dist: pandas>=2.3
|
|
28
29
|
Provides-Extra: test
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
codeaudit/__about__.py,sha256=
|
|
1
|
+
codeaudit/__about__.py,sha256=hKR4cqCW7w3epN3pUpdc-rArbI3mirHoz6l_BDd2xUw,122
|
|
2
2
|
codeaudit/__init__.py,sha256=vrz_p2yfDBQD-LBiT2hS0Nl6Da896DjCF4tBeg-3zmU,163
|
|
3
|
-
codeaudit/altairplots.py,sha256=
|
|
3
|
+
codeaudit/altairplots.py,sha256=mKM2JApYE5yDwV4qygAK1eDj3ykZ-YgXFnekH7iWyo0,35131
|
|
4
4
|
codeaudit/api_helpers.py,sha256=-B7XneWSfmYPfHnYRI149DtYTLjRf2Np3l0Tu6rNDQI,3810
|
|
5
|
-
codeaudit/api_interfaces.py,sha256=
|
|
6
|
-
codeaudit/api_reporting.py,sha256=
|
|
5
|
+
codeaudit/api_interfaces.py,sha256=4a-cTQRhRkcbdosEYNxR14D58gtTZUxE_T73BMcPBl4,32023
|
|
6
|
+
codeaudit/api_reporting.py,sha256=cHAv5FkwGzTfNI-4n5qNR-t7ZegpqBZjw1kUq8lpaVI,2054
|
|
7
|
+
codeaudit/benchmark_helpfunctions.py,sha256=Ju3RBrKJQpOkucOxiGFcIOzskuHu6fDjO3DfKBOPeHc,11151
|
|
7
8
|
codeaudit/checkmodules.py,sha256=gDQXD1D79zUPgbTtATy-CG7FQuZYLjrukbmXSF-eL08,6505
|
|
8
9
|
codeaudit/ci_workflowscan.py,sha256=ccA_38wTXPyGfWuGgaU9hGtt-HuX3f6cTh_EkaGRaWA,14285
|
|
9
10
|
codeaudit/complexitycheck.py,sha256=7ILf2wYho6DDiC-Q3ZjuJGbUZb1TaGfkTkWKtLO8_RU,4236
|
|
10
11
|
codeaudit/corecli.py,sha256=LRYPGhuPIpYwc_-fNq0wQITAPIoa6nIY0doHejS6q38,3958
|
|
11
12
|
codeaudit/dashboard_reports.py,sha256=A9e76T65ye158HQeqJHpuGzaaqx_oOnc5ChFfTXMz1A,17814
|
|
12
|
-
codeaudit/filehelpfunctions.py,sha256=
|
|
13
|
+
codeaudit/filehelpfunctions.py,sha256=7DA3QuTNiLyO7JFetoAIwDMK4oy6c3vHmZDqe04kCCY,5001
|
|
13
14
|
codeaudit/htmlhelpfunctions.py,sha256=z8vRqw9esLnk9Bsb7moOTLus5DSO34mQb4TYQfX6vd0,3247
|
|
14
15
|
codeaudit/issuevalidations.py,sha256=55QL-cZqdU7SH2ZiHniKtphsgeXZrEOoFJeRZyp1X5E,6437
|
|
15
16
|
codeaudit/privacy_lint.py,sha256=5t7Od16JNnb8SIGlEQy52c8vCxswnGO3SQbe-ilvqww,12439
|
|
16
|
-
codeaudit/pypi_package_scan.py,sha256=
|
|
17
|
-
codeaudit/reporting.py,sha256=
|
|
17
|
+
codeaudit/pypi_package_scan.py,sha256=htE-MBRzQT2qf-kvNFesohTeWfSzrGZl9ndJTEx-JOk,9152
|
|
18
|
+
codeaudit/reporting.py,sha256=Yg7UuFcn3ryhDiEv2vCJ6W_EhD_fWMpfspDAnQkCzWw,43688
|
|
18
19
|
codeaudit/security_checks.py,sha256=7nTlolMtI5MW-fhnOQonsp9W6-1QI1KDvNYO-NL5Am0,2149
|
|
19
20
|
codeaudit/simple.css,sha256=H7KT61oXJkVr9qXVrC5ME_Zph9jI-uR2IxOsXG1xs5k,4013
|
|
20
|
-
codeaudit/suppression.py,sha256=
|
|
21
|
+
codeaudit/suppression.py,sha256=Ku5EkFW6PlVZJ3vVCLkgvowXfYr8uELN_XVX95nvCas,6993
|
|
21
22
|
codeaudit/totals.py,sha256=i7Y5gXEk52p93Mo_usK2VATq1LmtBkFVKNGvT8AjJzw,6401
|
|
22
|
-
codeaudit/data/sastchecks.csv,sha256=
|
|
23
|
+
codeaudit/data/sastchecks.csv,sha256=9hhZun778xfQPRKl_b-i-HJ-KfDW0SmvBYh0FUL1Mws,11101
|
|
23
24
|
codeaudit/data/secretslist.txt,sha256=Vb-kuliUcuQHWAeuxIq3hZ-RNeKNXPNYIWoQe5SCTW4,1962
|
|
24
|
-
codeaudit-1.9.
|
|
25
|
-
codeaudit-1.9.
|
|
26
|
-
codeaudit-1.9.
|
|
27
|
-
codeaudit-1.9.
|
|
28
|
-
codeaudit-1.9.
|
|
25
|
+
codeaudit-1.9.4.dist-info/METADATA,sha256=KH3HsiilLjHpIH2bCALKoquUC5VIGSTrtfV_TU2Pzwo,9081
|
|
26
|
+
codeaudit-1.9.4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
27
|
+
codeaudit-1.9.4.dist-info/entry_points.txt,sha256=LSpeBIiwDjyffyx_YQIUzmBOnmdCJ_V42onGC_5Nv5E,53
|
|
28
|
+
codeaudit-1.9.4.dist-info/licenses/LICENSE.txt,sha256=-5gWaMGKJ54oX8TYP7oeg2zITdTapzyWl9PP0tispuA,34674
|
|
29
|
+
codeaudit-1.9.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|