codeanalyzer-python 0.2.1__py3-none-any.whl → 0.3.1__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.
- codeanalyzer/__main__.py +116 -6
- codeanalyzer/core.py +139 -213
- codeanalyzer/neo4j/__init__.py +1 -1
- codeanalyzer/neo4j/emit.py +2 -2
- codeanalyzer/neo4j/project.py +84 -18
- codeanalyzer/neo4j/schema.py +261 -15
- codeanalyzer/options/__init__.py +2 -2
- codeanalyzer/options/options.py +20 -1
- codeanalyzer/provenance.py +61 -0
- codeanalyzer/schema/py_schema.py +39 -1
- codeanalyzer/semantic_analysis/call_graph.py +24 -3
- codeanalyzer/semantic_analysis/pycg/__init__.py +20 -0
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +1054 -0
- codeanalyzer/semantic_analysis/{codeql/__init__.py → pycg/pycg_exceptions.py} +5 -8
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +401 -0
- codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +74 -10
- codeanalyzer/utils/logging.py +5 -2
- codeanalyzer/utils/progress_bar.py +15 -7
- codeanalyzer_python-0.3.1.dist-info/METADATA +553 -0
- codeanalyzer_python-0.3.1.dist-info/RECORD +39 -0
- {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/WHEEL +1 -1
- codeanalyzer/neo4j/catalog.py +0 -245
- codeanalyzer/semantic_analysis/codeql/codeql_analysis.py +0 -382
- codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py +0 -12
- codeanalyzer/semantic_analysis/codeql/codeql_loader.py +0 -91
- codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py +0 -185
- codeanalyzer_python-0.2.1.dist-info/METADATA +0 -415
- codeanalyzer_python-0.2.1.dist-info/RECORD +0 -39
- {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import platform
|
|
3
|
-
import stat
|
|
4
|
-
import zipfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import requests
|
|
8
|
-
from codeanalyzer.utils import logger
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CodeQLLoader:
|
|
12
|
-
@classmethod
|
|
13
|
-
def detect_platform_key(cls) -> str:
|
|
14
|
-
system = platform.system()
|
|
15
|
-
arch = platform.machine().lower()
|
|
16
|
-
|
|
17
|
-
if system == "Linux" and arch in {"x86_64", "amd64"}:
|
|
18
|
-
return "codeql-linux64.zip"
|
|
19
|
-
elif system == "Darwin" and arch in {"x86_64", "arm64"}:
|
|
20
|
-
return "codeql-osx64.zip"
|
|
21
|
-
elif system == "Windows" and arch in {"x86_64", "amd64"}:
|
|
22
|
-
return "codeql-win64.zip"
|
|
23
|
-
else:
|
|
24
|
-
return "codeql.zip" # fallback to generic binary if needed
|
|
25
|
-
|
|
26
|
-
@classmethod
|
|
27
|
-
def get_codeql_download_url(cls, expected_filename: str) -> str:
|
|
28
|
-
response = requests.get(
|
|
29
|
-
"https://api.github.com/repos/github/codeql-cli-binaries/releases/latest"
|
|
30
|
-
)
|
|
31
|
-
response.raise_for_status()
|
|
32
|
-
for asset in response.json()["assets"]:
|
|
33
|
-
if asset["name"] == expected_filename:
|
|
34
|
-
return asset["browser_download_url"]
|
|
35
|
-
raise RuntimeError(f"No asset found for filename: {expected_filename}")
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def download_and_extract_codeql(cls, temp_dir: Path) -> Path:
|
|
39
|
-
filename = cls.detect_platform_key()
|
|
40
|
-
download_url = cls.get_codeql_download_url(filename)
|
|
41
|
-
|
|
42
|
-
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
-
archive_path = temp_dir / filename
|
|
44
|
-
|
|
45
|
-
logger.info(f"Downloading CodeQL CLI from {download_url}")
|
|
46
|
-
with requests.get(download_url, stream=True) as r:
|
|
47
|
-
r.raise_for_status()
|
|
48
|
-
block_size = 8192 # 8KB
|
|
49
|
-
|
|
50
|
-
with open(archive_path, "wb") as f:
|
|
51
|
-
for chunk in r.iter_content(chunk_size=block_size):
|
|
52
|
-
f.write(chunk)
|
|
53
|
-
|
|
54
|
-
extract_dir = temp_dir / filename.replace(".zip", "")
|
|
55
|
-
extract_dir.mkdir(exist_ok=True)
|
|
56
|
-
|
|
57
|
-
logger.info(f"Extracting CodeQL CLI to {extract_dir}")
|
|
58
|
-
# zipfile.extractall drops Unix permissions (the executable bit), so
|
|
59
|
-
# we extract entries manually and copy each one's stored mode onto
|
|
60
|
-
# the file system. Without this, the CodeQL launcher script can't
|
|
61
|
-
# be executed and the next subprocess.Popen raises PermissionError.
|
|
62
|
-
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
|
63
|
-
for info in zip_ref.infolist():
|
|
64
|
-
extracted_path = zip_ref.extract(info, extract_dir)
|
|
65
|
-
stored_mode = info.external_attr >> 16
|
|
66
|
-
if stored_mode:
|
|
67
|
-
os.chmod(extracted_path, stored_mode)
|
|
68
|
-
|
|
69
|
-
# Archive is no longer needed once extracted.
|
|
70
|
-
try:
|
|
71
|
-
archive_path.unlink()
|
|
72
|
-
except OSError as exc:
|
|
73
|
-
logger.warning(f"Could not remove CodeQL archive {archive_path}: {exc}")
|
|
74
|
-
|
|
75
|
-
# rglob("codeql") returns both the launcher file *and* an internal
|
|
76
|
-
# directory of the same name (CodeQL ships its own runtime under
|
|
77
|
-
# ``codeql/codeql/``); insist on a regular file so we never bind to
|
|
78
|
-
# the directory.
|
|
79
|
-
codeql_bin = next(
|
|
80
|
-
(p for p in extract_dir.rglob("codeql") if p.is_file()),
|
|
81
|
-
None,
|
|
82
|
-
)
|
|
83
|
-
if not codeql_bin:
|
|
84
|
-
raise FileNotFoundError("CodeQL binary not found in extracted contents.")
|
|
85
|
-
|
|
86
|
-
# Belt-and-suspenders: ensure the binary is executable even if the
|
|
87
|
-
# archive entry's mode was zero (some older zip producers omit it).
|
|
88
|
-
st = codeql_bin.stat()
|
|
89
|
-
codeql_bin.chmod(st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
90
|
-
|
|
91
|
-
return codeql_bin.resolve()
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
################################################################################
|
|
2
|
-
# Copyright IBM Corporation 2025
|
|
3
|
-
#
|
|
4
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
# you may not use this file except in compliance with the License.
|
|
6
|
-
# You may obtain a copy of the License at
|
|
7
|
-
#
|
|
8
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
#
|
|
10
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
# See the License for the specific language governing permissions and
|
|
14
|
-
# limitations under the License.
|
|
15
|
-
################################################################################
|
|
16
|
-
|
|
17
|
-
"""Backend module for CodeQL query execution.
|
|
18
|
-
|
|
19
|
-
This module provides functionality to run CodeQL queries against CodeQL databases
|
|
20
|
-
and process the results.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
import shlex
|
|
24
|
-
import subprocess
|
|
25
|
-
import tempfile
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import List
|
|
28
|
-
|
|
29
|
-
import pandas as pd
|
|
30
|
-
from pandas import DataFrame
|
|
31
|
-
|
|
32
|
-
from codeanalyzer.semantic_analysis.codeql.codeql_exceptions import CodeQLExceptions
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class CodeQLQueryRunner:
|
|
36
|
-
"""A class for executing CodeQL queries against a CodeQL database.
|
|
37
|
-
|
|
38
|
-
This class provides a context manager interface for executing CodeQL queries
|
|
39
|
-
and handling temporary resources needed during query execution.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
database_path (str): The path to the CodeQL database.
|
|
43
|
-
codeql_bin (str | Path | None): Absolute path to the CodeQL CLI
|
|
44
|
-
binary. When ``None``, falls back to whatever ``codeql`` is on
|
|
45
|
-
``PATH``.
|
|
46
|
-
|
|
47
|
-
Attributes:
|
|
48
|
-
database_path (Path): The path to the CodeQL database.
|
|
49
|
-
codeql_bin (str): Resolved binary path or the literal ``"codeql"``.
|
|
50
|
-
temp_file_path (Path): The path to the temporary query file.
|
|
51
|
-
csv_output_file (Path): The path to the CSV output file.
|
|
52
|
-
temp_bqrs_file_path (Path): The path to the temporary bqrs file.
|
|
53
|
-
temp_qlpack_file (Path): The path to the temporary qlpack file.
|
|
54
|
-
|
|
55
|
-
Raises:
|
|
56
|
-
CodeQLQueryExecutionException: If there is an error executing the query.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
def __init__(self, database_path: str, codeql_bin=None, codeql_packs_dir=None):
|
|
60
|
-
self.database_path: Path = Path(database_path)
|
|
61
|
-
self.codeql_bin: str = str(codeql_bin) if codeql_bin else "codeql"
|
|
62
|
-
self.codeql_packs_dir = (
|
|
63
|
-
Path(codeql_packs_dir) if codeql_packs_dir is not None else None
|
|
64
|
-
)
|
|
65
|
-
self.temp_file_path: Path = None
|
|
66
|
-
|
|
67
|
-
def __enter__(self):
|
|
68
|
-
"""Context entry that prepares paths to execute a CodeQL query.
|
|
69
|
-
|
|
70
|
-
The ``.ql`` file is written **inside the prepared qlpack
|
|
71
|
-
directory** (``codeql_packs_dir``) so ``import python`` resolves
|
|
72
|
-
against that pack's installed dependencies — no
|
|
73
|
-
``--additional-packs`` or ``--search-path`` needed. The CSV /
|
|
74
|
-
BQRS output files live in ``tempfile`` because they're transient
|
|
75
|
-
per-query artifacts.
|
|
76
|
-
"""
|
|
77
|
-
# CSV and BQRS files are transient per-query — fine in /tmp.
|
|
78
|
-
csv_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".csv")
|
|
79
|
-
bqrs_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".bqrs")
|
|
80
|
-
self.csv_output_file = Path(csv_file.name)
|
|
81
|
-
self.temp_bqrs_file_path = Path(bqrs_file.name)
|
|
82
|
-
csv_file.close()
|
|
83
|
-
bqrs_file.close()
|
|
84
|
-
|
|
85
|
-
# The .ql file MUST live inside the prepared qlpack so its
|
|
86
|
-
# ``import python`` resolves via that pack's lock file. Writing
|
|
87
|
-
# outside the pack means CodeQL falls back to a default
|
|
88
|
-
# search-path that doesn't include downloaded library packs.
|
|
89
|
-
if self.codeql_packs_dir is None:
|
|
90
|
-
raise RuntimeError(
|
|
91
|
-
"CodeQLQueryRunner requires codeql_packs_dir — the directory "
|
|
92
|
-
"of an installed qlpack that depends on codeql/python-all."
|
|
93
|
-
)
|
|
94
|
-
ql_file = tempfile.NamedTemporaryFile(
|
|
95
|
-
"w", delete=False, suffix=".ql", dir=str(self.codeql_packs_dir)
|
|
96
|
-
)
|
|
97
|
-
self.temp_file_path = Path(ql_file.name)
|
|
98
|
-
ql_file.close()
|
|
99
|
-
|
|
100
|
-
return self
|
|
101
|
-
|
|
102
|
-
def execute(self, query_string: str, column_names: List[str]) -> DataFrame:
|
|
103
|
-
"""Writes the query to the temporary file and executes it against the specified CodeQL database.
|
|
104
|
-
|
|
105
|
-
Args:
|
|
106
|
-
query_string (str): The CodeQL query string to be executed.
|
|
107
|
-
column_names (List[str]): The list of column names for the CSV the CodeQL produces when we execute the query.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
dict: A dictionary containing the resulting DataFrame.
|
|
111
|
-
|
|
112
|
-
Raises:
|
|
113
|
-
RuntimeError: If the context manager is not entered using the 'with' statement.
|
|
114
|
-
CodeQLQueryExecutionException: If there is an error executing the query.
|
|
115
|
-
"""
|
|
116
|
-
if not self.temp_file_path:
|
|
117
|
-
raise RuntimeError("CodeQLQueryRunner not entered using 'with' statement.")
|
|
118
|
-
|
|
119
|
-
# Write the query to the temp file so we can execute it.
|
|
120
|
-
self.temp_file_path.write_text(query_string)
|
|
121
|
-
|
|
122
|
-
# The .ql file sits inside the qlpack directory whose lock file
|
|
123
|
-
# already resolves ``codeql/python-all`` and its transitive
|
|
124
|
-
# dependencies. ``codeql query run`` auto-discovers the enclosing
|
|
125
|
-
# qlpack — no extra flags required.
|
|
126
|
-
codeql_query_cmd = shlex.split(
|
|
127
|
-
f"{shlex.quote(self.codeql_bin)} query run {self.temp_file_path} "
|
|
128
|
-
f"--database={self.database_path} "
|
|
129
|
-
f"--output={self.temp_bqrs_file_path}",
|
|
130
|
-
posix=False,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
call = subprocess.Popen(
|
|
134
|
-
codeql_query_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
135
|
-
)
|
|
136
|
-
_, err = call.communicate()
|
|
137
|
-
if call.returncode != 0:
|
|
138
|
-
raise CodeQLExceptions.CodeQLQueryExecutionException(
|
|
139
|
-
f"Error executing query: {(err or b'').decode(errors='replace')}"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
# Convert the bqrs file to a CSV file
|
|
143
|
-
bqrs2csv_command = shlex.split(
|
|
144
|
-
f"{shlex.quote(self.codeql_bin)} bqrs decode --format=csv --output={self.csv_output_file} {self.temp_bqrs_file_path}",
|
|
145
|
-
posix=False,
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
# Read the CSV file content and cast it to a DataFrame
|
|
149
|
-
|
|
150
|
-
call = subprocess.Popen(
|
|
151
|
-
bqrs2csv_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
152
|
-
)
|
|
153
|
-
_, err = call.communicate()
|
|
154
|
-
if call.returncode != 0:
|
|
155
|
-
raise CodeQLExceptions.CodeQLQueryExecutionException(
|
|
156
|
-
f"Error decoding bqrs: {(err or b'').decode(errors='replace')}"
|
|
157
|
-
)
|
|
158
|
-
else:
|
|
159
|
-
return pd.read_csv(
|
|
160
|
-
self.csv_output_file,
|
|
161
|
-
header=None,
|
|
162
|
-
names=column_names,
|
|
163
|
-
skiprows=[0],
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
167
|
-
"""Clean up resources used by the CodeQL analysis.
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
exc_type: The exception type if an exception was raised in the context, otherwise None.
|
|
171
|
-
exc_val: The exception instance if an exception was raised in the context, otherwise None.
|
|
172
|
-
exc_tb: The traceback if an exception was raised in the context, otherwise None.
|
|
173
|
-
|
|
174
|
-
Note:
|
|
175
|
-
Deletes the temporary files created during the analysis, including the temporary file path,
|
|
176
|
-
the CSV output file, and the temporary QL pack file.
|
|
177
|
-
"""
|
|
178
|
-
if self.temp_file_path and self.temp_file_path.exists():
|
|
179
|
-
self.temp_file_path.unlink()
|
|
180
|
-
|
|
181
|
-
if self.csv_output_file and self.csv_output_file.exists():
|
|
182
|
-
self.csv_output_file.unlink()
|
|
183
|
-
|
|
184
|
-
if self.temp_bqrs_file_path and self.temp_bqrs_file_path.exists():
|
|
185
|
-
self.temp_bqrs_file_path.unlink()
|