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 CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Maikel Mardjan
2
2
  #
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
- __version__ = "1.9.2"
4
+ __version__ = "1.9.4"
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
- # --- Count every 'validation' across all files ---
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": len(sast_result),
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
- for file_info in file_security_info.values():
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(
@@ -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): # check on parseable single Python file
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
- Analyze a Python file or package and count occurrences of code weaknesses.
319
- """
320
- # Assuming filescan is imported from codeaudit.api_interfaces
321
- scan_result = filescan(input_file, nosec)
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("filescan() did not return a valid result dictionary")
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
- # Based on your data: sast_result keys are line numbers (e.g., 171)
344
- # and values are dicts containing the 'validation' type.
345
- for issue_details in sast_result.values():
346
- if isinstance(issue_details, dict):
347
- # We use the 'validation' field to identify the type of weakness
348
- weakness_type = issue_details.get("validation", "Unknown")
349
- counter[weakness_type] += 1
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.
@@ -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
- """Returns the total weaknesses found
25
- input: scan_result (dict): Scan output containing "file_security_info" with
26
- per-file SAST findings. Each finding should include a "validation" field.
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
- for file_info in scan_result.get("file_security_info", {}).values():
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
- total += len(sast_result)
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
+
@@ -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,Low,User input must be strictly sanitized and validated to prevent injection vulnerabilities.
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.
@@ -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
- Checks whether a Python file can be validated and parsed.
103
- Uses the extensive security checker for valid Python files of the module https://github.com/nocomplexity/fileaudit
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 Python validation, False otherwise.
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
- return validate_python(Path(file_path))
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):
@@ -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
- return (
98
- path.endswith(".tar.gz")
99
- and validate_tar_gz(
100
- path,
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
- # def is_valid_tar_gz(path):
111
- # """Return True if the given path is a valid .tar.gz file.
112
- # Custom validation values for validate_tar_gz are used. So note that NOT all tar.gz files will be accepted. Use PyPI package scan instead if possible.
113
- # """
114
- # return bool(
115
- # validate_tar_gz(path, max_file_size=15 * 1024 * 1024, max_tar_members=12000)
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 validate_tar_gz(
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>&#128073; 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\-]+", normalized)
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: codeaudit
3
- Version: 1.9.2
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=lEwjjJlm2CvFjGxH6luX5N-xGFvuupCM1jTTMibyA_w,122
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=WsLmKqZ_sEysAv7q668WDnOlENk7YyHax24AwRihHC8,35906
3
+ codeaudit/altairplots.py,sha256=mKM2JApYE5yDwV4qygAK1eDj3ykZ-YgXFnekH7iWyo0,35131
4
4
  codeaudit/api_helpers.py,sha256=-B7XneWSfmYPfHnYRI149DtYTLjRf2Np3l0Tu6rNDQI,3810
5
- codeaudit/api_interfaces.py,sha256=yCkoIIWaHuF7QCxz2Z2ufbIGcvCrUEdolQirg-zfjxk,31193
6
- codeaudit/api_reporting.py,sha256=WRXPsS0t9ljdD_6gN5uN-t0xqNOuE9k5oqSmxaYWTf8,1585
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=oYdCoAMeNlik7KeaQ7tHs-lM0jdXjEfWykW0M9wtcE8,4221
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=iqpFcG3-UHeGrOrDwNIP8SQOnEpLmcXYj9fKq3bepO8,9518
17
- codeaudit/reporting.py,sha256=ZCc-NY4h9WwY7_Nfm6WjPyfWQ8s6eArNA0_hn9pF4vQ,42961
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=8ZX8NAoWxJjgP3ZruvuQz5G_XFmPvyjOAYF84Q6bT2M,7755
21
+ codeaudit/suppression.py,sha256=Ku5EkFW6PlVZJ3vVCLkgvowXfYr8uELN_XVX95nvCas,6993
21
22
  codeaudit/totals.py,sha256=i7Y5gXEk52p93Mo_usK2VATq1LmtBkFVKNGvT8AjJzw,6401
22
- codeaudit/data/sastchecks.csv,sha256=qljGNtwDff-GypMhiSK2KYABWUzpOH5aetrzFbU7PDA,11100
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.2.dist-info/METADATA,sha256=w4M3E7DmLwvLMfFgA0wdlhtQmmzMvWY6vcoiJXNuphc,9023
25
- codeaudit-1.9.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
26
- codeaudit-1.9.2.dist-info/entry_points.txt,sha256=LSpeBIiwDjyffyx_YQIUzmBOnmdCJ_V42onGC_5Nv5E,53
27
- codeaudit-1.9.2.dist-info/licenses/LICENSE.txt,sha256=-5gWaMGKJ54oX8TYP7oeg2zITdTapzyWl9PP0tispuA,34674
28
- codeaudit-1.9.2.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any