codeaudit 1.4.1__py3-none-any.whl → 1.5.0__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 <mike@bm-support.org>
2
2
  #
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
- __version__ = "1.4.1"
4
+ __version__ = "1.5.0"
@@ -18,7 +18,7 @@ from codeaudit.filehelpfunctions import get_filename_from_path , collect_python_
18
18
  from codeaudit.security_checks import perform_validations , ast_security_checks
19
19
  from codeaudit.totals import overview_per_file , get_statistics , overview_count , total_modules
20
20
  from codeaudit.checkmodules import get_all_modules , get_imported_modules_by_file , get_standard_library_modules , check_module_vulnerability
21
-
21
+ from codeaudit.pypi_package_scan import get_pypi_download_info , get_package_source
22
22
 
23
23
  from pathlib import Path
24
24
  import json
@@ -35,10 +35,63 @@ def version():
35
35
  return {"name" : "Python_Code_Audit",
36
36
  "version" : ca_version}
37
37
 
38
-
39
38
  def filescan(input_path):
40
- """Scans a Python file or directory and returns result as JSON"""
41
- output ={}
39
+ """
40
+ Scan a Python source file, a local directory, or a **PyPI package** from PyPI.org for
41
+ security weaknesses and return the results as a JSON-serializable
42
+ dictionary.
43
+
44
+ This API function works on:
45
+
46
+ - **Local directory**: Recursively scans all supported Python files in the
47
+ directory.
48
+ - **Single Python file**: Scans the file if it exists and can be parsed
49
+ into an AST.
50
+ - **PyPI package** on PyPI.org: Downloads the
51
+ source distribution from PyPI, scans it, and cleans up temporary files.
52
+
53
+ The returned output always includes Python Code Audit version information and a
54
+ generation timestamp. For consistency, single-file scans are normalized
55
+ to match the structure of directory/package scans.
56
+
57
+ **Note:**
58
+ The filescan command does NOT include all directories. This is done on purpose!
59
+ The following directories are skipped by default:
60
+
61
+ - `/docs`
62
+ - `/docker`
63
+ - `/dist`
64
+ - `/tests`
65
+ - all directories that start with . (dot) or _ (underscore)
66
+
67
+ But you can easily change this if needed!
68
+
69
+ Args:
70
+ input_path (str): One of the following:
71
+ - Path to a local directory containing Python code.
72
+ - Path to a single ``.py`` file.
73
+ - Name of a package available on PyPI.
74
+
75
+ Returns:
76
+ dict: A JSON-serializable dictionary containing scan results and
77
+ metadata. The structure varies slightly depending on the scan type,
78
+ but always includes:
79
+ - Version information from ``version()``.
80
+ - ``generated_on`` timestamp (``YYYY-MM-DD HH:MM``).
81
+ - Package or file-level security findings.
82
+
83
+ If the input cannot be interpreted as a valid directory, Python file,
84
+ or PyPI package, a dictionary with an ``"Error"`` key is returned.
85
+
86
+ Raises:
87
+ None explicitly. Any unexpected exceptions are allowed to propagate
88
+ unless handled by downstream callers.
89
+
90
+ Example:
91
+ >>> result = filescan("example_package")
92
+ >>> result["package_name"]
93
+
94
+ """
42
95
  file_output = {}
43
96
  file_path = Path(input_path)
44
97
  ca_version_info = version()
@@ -46,35 +99,37 @@ def filescan(input_path):
46
99
  timestamp_str = now.strftime("%Y-%m-%d %H:%M")
47
100
  output = ca_version_info | {"generated_on" : timestamp_str}
48
101
  # Check if the input is a valid directory or a single valid Python file
49
- if file_path.is_dir():
50
- files_to_check = collect_python_source_files(input_path)
51
- if len(files_to_check) > 1:
52
- modules_discovered = get_all_modules(input_path) #all modules for the package aka directory
53
- name_of_package = get_filename_from_path(input_path)
54
- package_overview = get_overview(input_path)
55
- output |= {"package_name" : name_of_package ,
56
- "statistics_overview" : package_overview ,
57
- "module_overview" : modules_discovered }
58
- for i,file in enumerate(files_to_check):
59
- file_information = overview_per_file(file)
60
- module_information = get_modules(file) # modules per file
61
- scan_output = _codeaudit_scan(file)
62
- file_output[i] = file_information | module_information | scan_output
63
- output |= { "file_security_info" : file_output}
64
- return output
65
- else:
66
- output_msg = f'Directory path {input_path} contains no Python files.'
67
- return {"Error" : output_msg}
102
+ if file_path.is_dir(): #local directory scan
103
+ package_name = get_filename_from_path(input_path)
104
+ output |= {"package_name": package_name}
105
+ scan_output = _codeaudit_directory_scan(input_path)
106
+ output |= scan_output
107
+ return output
68
108
  elif file_path.suffix.lower() == ".py" and file_path.is_file() and is_ast_parsable(input_path): #check on parseable single Python file
69
- #do a file check
109
+ # do a file check
70
110
  file_information = overview_per_file(input_path)
71
111
  module_information = get_modules(input_path) # modules per file
72
112
  scan_output = _codeaudit_scan(input_path)
73
- file_output[0] = file_information | module_information | scan_output #there is only 1 file , so index 0 equals as for package to make functionality that use the output that works on the dict or json can equal for a package or a single file!
113
+ file_output["0"] = file_information | module_information | scan_output #there is only 1 file , so index 0 equals as for package to make functionality that use the output that works on the dict or json can equal for a package or a single file!
74
114
  output |= { "file_security_info" : file_output}
75
115
  return output
116
+ elif (pypi_data := get_pypi_download_info(input_path)):
117
+ package_name = input_path #The variable input_path is now equal to the package name
118
+ url = pypi_data['download_url']
119
+ release = pypi_data['release']
120
+ if url is not None:
121
+ src_dir, tmp_handle = get_package_source(url)
122
+ output |= {"package_name": package_name,
123
+ "package_release": release}
124
+ try:
125
+ scan_output = _codeaudit_directory_scan(src_dir)
126
+ output |= scan_output
127
+ finally:
128
+ # Cleaning up temp directory
129
+ tmp_handle.cleanup() # deletes everything from temp directory
130
+ return output
76
131
  else:
77
- #Its not a directory nor a valid Python file:
132
+ # Its not a directory nor a valid Python file:
78
133
  return {"Error" : "File is not a *.py file, does not exist or is not a valid directory path towards a Python package."}
79
134
 
80
135
  def _codeaudit_scan(filename):
@@ -90,6 +145,30 @@ def _codeaudit_scan(filename):
90
145
  "sast_result": sast_result}
91
146
  return output
92
147
 
148
+ def _codeaudit_directory_scan(input_path):
149
+ """Performs a scan on a local directory
150
+ Function is also used with scanning directory PyPI.org packages, since in that case a tmp directory is used
151
+ """
152
+ output ={}
153
+ file_output = {}
154
+ files_to_check = collect_python_source_files(input_path)
155
+ if len(files_to_check) > 1:
156
+ modules_discovered = get_all_modules(input_path) #all modules for the package aka directory
157
+ package_overview = get_overview(input_path)
158
+ output |= {"statistics_overview" : package_overview ,
159
+ "module_overview" : modules_discovered }
160
+ for i,file in enumerate(files_to_check):
161
+ file_information = overview_per_file(file)
162
+ module_information = get_modules(file) # modules per file
163
+ scan_output = _codeaudit_scan(file)
164
+ file_output[i] = file_information | module_information | scan_output
165
+ output |= { "file_security_info" : file_output}
166
+ return output
167
+ else:
168
+ output_msg = f'Directory path {input_path} contains no Python files.'
169
+ return {"Error" : output_msg}
170
+
171
+
93
172
  def save_to_json(sast_result, filename="codeaudit_output.json"):
94
173
  """
95
174
  Save a SAST result (dict or serializable object) to a JSON file.
@@ -208,13 +287,39 @@ def get_overview(input_path):
208
287
  return {"Error" : "File is not a *.py file, does not exist or is not a valid directory path to a Python package."}
209
288
 
210
289
  def get_default_validations():
211
- """Retrieves the implemented default security validations
212
- Args:
213
- none
290
+ """Retrieve the default implemented security validations.
291
+
292
+ This function collects the built-in Static Application Security Testing (SAST)
293
+ validations applied to standard Python modules. It retrieves the validation
294
+ definitions, converts them into a serializable format, and enriches the result
295
+ with generation metadata.
296
+
297
+ The returned structure is intended to be consumed by reporting, API, or
298
+ documentation layers.
214
299
 
215
300
  Returns:
216
- dict: Overview of implemented security SAST validation on Standard Python modules. Including vital help text.
217
- """
301
+ dict: A dictionary containing generation metadata and a list of security
302
+ validations. The dictionary has the following structure:
303
+
304
+ {
305
+ "<metadata_key>": <metadata_value>,
306
+ ...,
307
+ "validations": [
308
+ {
309
+ "<field>": <value>,
310
+ ...
311
+ },
312
+ ...
313
+ ]
314
+ }
315
+
316
+
317
+ **Notes**:
318
+
319
+ - Requires Python 3.9 or later due to use of the dictionary union operator (`|`).
320
+ - The `validations` list is derived from a pandas DataFrame using
321
+ `to_dict(orient="records")`.
322
+ """
218
323
  df = ast_security_checks()
219
324
  result = df.to_dict(orient="records")
220
325
  output = _generation_info() | {"validations" : result}
@@ -255,15 +360,16 @@ def get_psl_modules():
255
360
  return output
256
361
 
257
362
  def get_module_vulnerability_info(module):
258
- """Retrieves vulnerability information for external modules using the OSV Database
363
+ """
364
+ Retrieves vulnerability information for an external module using the OSV Database.
365
+
259
366
  Args:
260
- input: module name
261
-
367
+ module (str): Name of the module to query.
368
+
262
369
  Returns:
263
- dict: Result of OSV query
264
- """
370
+ dict: Generation metadata combined with OSV vulnerability results.
371
+ """
265
372
  vuln_info = check_module_vulnerability(module)
266
373
  key_string = f'{module}_vulnerability_info'
267
374
  output = _generation_info() | { key_string : vuln_info}
268
375
  return output
269
-
codeaudit/codeaudit.py CHANGED
@@ -45,29 +45,32 @@ def display_help():
45
45
  docstring = function.__doc__.strip().split('\n')[0] or ""
46
46
  summary = docstring.split("\n", 1)[0]
47
47
  print(f" {command:<20} {summary}")
48
- print("\nUse the Codeaudit documentation to check the security of Python programs and make your Python programs more secure!\nCheck https://simplifysecurity.nocomplexity.com/ \n")
49
-
48
+ print("\nUse the Python Code Audit documentation (https://codeaudit.nocomplexity.com) to audit and secure your Python programmes. Explore further essential open-source security tools at https://simplifysecurity.nocomplexity.com/\n")
50
49
 
51
50
  def main():
51
+ if "-?" in sys.argv: # Normalize help flags BEFORE Fire sees them: fire module treats anything starting with - as a flag/value, not as a help alias.
52
+ sys.argv[sys.argv.index("-?")] = "--help"
53
+ if "-help" in sys.argv: # Normalize help flags BEFORE Fire sees them
54
+ sys.argv[sys.argv.index("-help")] = "--help"
52
55
  if len(sys.argv) > 1 and sys.argv[1] in ("-v", "--v", "--version", "-version"):
53
56
  display_version()
54
- elif len(sys.argv) > 1 and sys.argv[1] in ("-help", "-?", "--help", "-h"):
57
+ elif len(sys.argv) > 1 and sys.argv[1] in ("-help", "--help", "-h"):
55
58
  display_help()
56
59
  elif len(sys.argv) == 1:
57
60
  display_help()
58
61
  else:
59
62
  fire.Fire(
60
63
  {
61
- "overview": overview_report,
64
+ "overview": overview_report,
62
65
  "modulescan": report_module_information,
63
- "filescan" : scan_report,
64
- "checks" : report_implemented_tests,
65
- "version" : display_version,
66
- "-help": display_help,
66
+ "filescan": scan_report,
67
+ "checks": report_implemented_tests,
68
+ "version": display_version,
67
69
  }
68
70
  )
69
71
 
70
72
 
73
+
71
74
  if __name__ == "__main__":
72
75
  main()
73
76
 
@@ -0,0 +1,135 @@
1
+
2
+ _KEY
3
+ _passwd
4
+ _PASSWORD
5
+ access_key
6
+ access_key_id
7
+ ACCESS_SECRET
8
+ ACCESS_TOKEN
9
+ AccountKey
10
+ AI21_API_KEY
11
+ ALIBABA_CLOUD_ACCESS_KEY_ID
12
+ ALIBABA_CLOUD_ACCESS_KEY_SECRET
13
+ ANTHROPIC_API_KEY
14
+ api_key
15
+ API_TOKEN
16
+ ApiKey
17
+ ApiSecret
18
+ APP_KEY
19
+ APP_SECRET
20
+ AUTH
21
+ auth_key
22
+ AUTH_SECRET
23
+ auth_token
24
+ AUTH_TOKEN
25
+ Authorization
26
+ AWS_ACCESS_KEY_ID
27
+ aws_account_id
28
+ aws_secret_access_key
29
+ AWS_SECRET_ACCESS_KEY
30
+ aws_session_token
31
+ AWS_SESSION_TOKEN
32
+ AZURE_OPENAI_API_KEY
33
+ AZURE_OPENAI_API_VERSION
34
+ AZURE_OPENAI_ENDPOINT
35
+ AzureStorageKey
36
+ BAIDU_API_KEY
37
+ BAIDU_SECRET_KEY
38
+ BASIC_AUTH
39
+ BEARER
40
+ BEARER_TOKEN
41
+ BEDROCK_REGION
42
+ CLIENT_ID
43
+ client_key
44
+ CLIENT_SECRET
45
+ ClientSecret
46
+ COHERE_API_KEY
47
+ CONNECTION_STRING
48
+ credential
49
+ credentials
50
+ CREDENTIALS_JSON
51
+ creds
52
+ CSRF_TOKEN
53
+ DASHSCOPE_API_KEY
54
+ DEEPSEEK_API_KEY
55
+ DEPLOY_KEY
56
+ encryptedPassword
57
+ ENCRYPTION_SECRET
58
+ EncryptionKey
59
+ FERNET_KEY
60
+ FIREWORKS_API_KEY
61
+ GCP_SERVICE_ACCOUNT_KEY
62
+ GEMINI_API_KEY
63
+ get_api_token
64
+ get_secret
65
+ get_token
66
+ GITHUB_TOKEN
67
+ GOOGLE_API_KEY
68
+ GOOGLE_API_KEY
69
+ HMAC_KEY
70
+ HUGGINGFACE_API_TOKEN
71
+ IBM_WATSONX_API_KEY
72
+ IBM_WATSONX_PROJECT_ID
73
+ ID_TOKEN
74
+ INTEGRATION_KEY
75
+ JWT_ACCESS_TOKEN
76
+ JWT_ALGORITHM
77
+ JWT_AUDIENCE
78
+ JWT_ISSUER
79
+ JWT_PRIVATE_KEY
80
+ JWT_PUBLIC_KEY
81
+ JWT_REFRESH_TOKEN
82
+ JWT_SECRET
83
+ JWT_SECRET_KEY
84
+ JWT_SIGNING_KEY
85
+ JWT_TOKEN
86
+ KEYFILE
87
+ KUBE_TOKEN
88
+ MASTER_KEY
89
+ MISTRAL_API_KEY
90
+ MLAB_PASS
91
+ MOONSHOT_API_KEY
92
+ NetworkCredential
93
+ NVIDIA_API_KEY
94
+ OAUTH_TOKEN
95
+ OLLAMA_API_BASE
96
+ OPENAI_API_KEY
97
+ OPENROUTER_API_KEY
98
+ OTEL_EXPORTER
99
+ PASSPHRASE
100
+ password
101
+ POSTGRES_PASSWORD
102
+ PPLX_API_KEY
103
+ PRIVATE_KEY
104
+ PRIVATE_TOKEN
105
+ REDIS_PASSWORD
106
+ REFRESH_TOKEN
107
+ REPLICATE_API_TOKEN
108
+ ROOT_PASSWORD
109
+ RSA_PRIVATE_KEY
110
+ SAS_TOKEN
111
+ secret
112
+ secret_key
113
+ secret_key_base
114
+ SECRET_TOKEN
115
+ SERVICE_ACCOUNT_KEY
116
+ SESSION_KEY
117
+ SIGNING_KEY
118
+ SILICONFLOW_API_KEY
119
+ SLACK_TOKEN
120
+ SMTP_PASSWORD
121
+ SSH_KEY
122
+ static_key
123
+ STRIPE_API_KEY
124
+ SYSTEM_PASSWORD
125
+ TENCENT_HUNYUAN_API_KEY
126
+ TLS_PRIVATE_KEY
127
+ TOGETHER_API_KEY
128
+ TOKEN
129
+ VAULT_TOKEN
130
+ WEBHOOK_SECRET
131
+ WEBHOOK_TOKEN
132
+ X_API_KEY
133
+ XAI_API_KEY
134
+ YI_API_KEY
135
+ ZHIPUAI_API_KEY
@@ -24,7 +24,7 @@ def read_in_source_file(file_path):
24
24
 
25
25
  if file_path.is_dir():
26
26
  print(
27
- "Error: The given path is a directory.\nUse 'codeaudit directoryscan' to audit all Python files in a directory.\nThe 'codeaudit modulescan' command works per file only, not on a directory.\nUse codeaudit -h for help"
27
+ "Error: The given path is a directory.\nUse 'codeaudit filescan' to security audit Python files in a directory or PyPI package.\nThe 'codeaudit modulescan' command works per file only, not on a directory.\nUse codeaudit -h for help"
28
28
  )
29
29
  sys.exit(1)
30
30