Skip to content

APPENG-2959: Add python transitive support on top of NAT framework#100

Merged
TamarW0 merged 1 commit into
rh-aiq-mainfrom
APPENG-2959-migration
Sep 8, 2025
Merged

APPENG-2959: Add python transitive support on top of NAT framework#100
TamarW0 merged 1 commit into
rh-aiq-mainfrom
APPENG-2959-migration

Conversation

@TamarW0

@TamarW0 TamarW0 commented Sep 1, 2025

Copy link
Copy Markdown
Collaborator

No description provided.

@TamarW0 TamarW0 requested a review from zvigrinberg September 1, 2025 14:01

@zvigrinberg zvigrinberg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @TamarW0
Good job!.
Please see my comments, some of them requires changes.
Thanks!.

Comment thread src/vuln_analysis/functions/cve_agent.py Outdated
Comment thread src/vuln_analysis/utils/transitive_code_searcher_tool.py
Comment thread src/vuln_analysis/utils/python_segmenters_with_claases_methods.py
Comment thread src/vuln_analysis/utils/document_embedding.py
Comment thread src/vuln_analysis/utils/chain_of_calls_retriever.py
Comment thread src/vuln_analysis/utils/chain_of_calls_retriever.py Outdated
Comment thread src/vuln_analysis/utils/chain_of_calls_retriever.py Outdated
Comment thread src/vuln_analysis/utils/chain_of_calls_retriever.py Outdated
@zvigrinberg zvigrinberg self-requested a review September 4, 2025 05:25

@zvigrinberg zvigrinberg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @TamarW0,
I Have few minor comments, please take a look.

Thanks.

Comment thread src/vuln_analysis/utils/chain_of_calls_retriever.py
Comment thread src/vuln_analysis/utils/dep_tree.py Outdated
Comment thread src/vuln_analysis/utils/dep_tree.py Outdated
Comment on lines +342 to +514


class PythonDependencyTreeBuilder(DependencyTreeBuilder):

def build_tree(self, manifest_path: Path) -> defaultdict[Any, list]:
cmd = f'{manifest_path}/{TRANSITIVE_ENV_NAME}/bin/python -m pip install deptree'
run_command(cmd)
cmd = f'{manifest_path}/{TRANSITIVE_ENV_NAME}/bin/deptree',
dependencies = run_command(cmd)
parent_stack = []
tree = defaultdict(set)
ROOT_PROJECT = 'root_project'
tree[ROOT_PROJECT] = [ROOT_LEVEL_SENTINEL]
for line in dependencies.split(os.linesep):
level = 0
while line.startswith(' '):
level += 1
line = line[2:]
package = line.split('==')[0].strip().lower()
package = package.replace('-', '_')
if level < len(parent_stack):
parent_stack = parent_stack[:level]
try:
tree[package].add(parent_stack[-1])
except IndexError:
pass
parent_stack.append(package)

installed_dependencies = []
with open(manifest_path / PYTHON_MANIFEST, 'r') as manifest:
for line in manifest:
if line.strip() and not PythonLanguageFunctionsParser.is_comment_line(line):
installed_dependencies.append(re.split(r"[=>< ]", line.strip())[0])
for dependency, parents in tree.items():
if dependency in installed_dependencies:
parents.add(ROOT_PROJECT)
tree[dependency] = list(parents)
return tree

def extract_version_from_specifier(self, specifier_str: str) -> Optional[str]:
"""
Extracts a most likely specific Python version from a PEP 440 specifier string.
Prioritizes exact matches, then lower bounds.
Examples:
"==3.9" -> "3.9"
">=3.8,<4.0" -> "3.8"
"~=3.7" -> "3.7" (if ~=3.7 is equivalent to >=3.7,<3.8)
">3.8" -> "3.9" (returns the next major.minor version)
"""
try:
specifier_set = SpecifierSet(specifier_str)

for specifier in specifier_set:
if specifier.operator in ('==', '==='):
return specifier.version

lower_bounds = []
for specifier in specifier_set:
if specifier.operator in ('>=',):
lower_bounds.append(self.parse_version(specifier.version))
if lower_bounds:
# Return the highest (most restrictive) lower bound
highest_lower_bound = max(lower_bounds)
return str(highest_lower_bound)

# Look for a compatible release specifier (~=3.7 implies >=3.7,<3.8)
compatible_match = re.search(r'~=(\d+\.\d+)', specifier_str)
if compatible_match:
return compatible_match.group(1)

# As a fallback for ">X.Y" which means X.Y+1.0, return the next minor version
greater_than_match = re.search(r'>(\d+\.\d+)', specifier_str)
if greater_than_match:
major, minor = map(int, greater_than_match.group(1).split('.'))
return f"{major}.{minor + 1}"

except Exception as e: # Catch parsing errors from packaging.specifiers
logger.warning(f"Warning: Could not parse specifier '{specifier_str}': {e}")
return None

def extract_version_from_readme_hint(self, line: str) -> Optional[str]:
"""
Extracts a Python version (e.g., 3.8, 3.12) from a free-form text line.
Looks for patterns like 'Python 3.9', 'python3.10', 'py3.11'.
"""
match = re.search(r'(?:Python|python|py)\s*(\d+\.\d+)', line, re.IGNORECASE)
return match.group(1) if match else None

def extract_version_from_pyproject_toml(self, content: str) -> Optional[str]:
#todo: implement more options here, see example: https://github.com/quay/quay/blob/master/pyproject.toml
try:
pyproject_data = json.loads(content)
specifier_from_metadata = pyproject_data.get('project', {}).get('requires-python', None)
if specifier_from_metadata:
logger.debug(f"Found requires-python in pyproject.toml: '{specifier_from_metadata}'")
# Try to extract a specific version from the specifier
specific_version = self.extract_version_from_specifier(specifier_from_metadata)
if specific_version:
return specific_version # Return the most authoritative version immediately
return ''
except json.JSONDecodeError:
logger.warning(f"Warning: Failed to parse pyproject.toml as JSON.")

def extract_version_from_setup_py(self, content: str):
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'setup':
for keyword in node.keywords:
if keyword.arg == 'python_requires':
if isinstance(keyword.value, ast.Constant):
specifier_from_metadata = keyword.value.value
logger.debug(f"Found python_requires in setup.py: '{specifier_from_metadata}'")
specific_version = self.extract_version_from_specifier(specifier_from_metadata)
if specific_version:
return specific_version
break
except SyntaxError:
logger.warning(f"Warning: Failed to parse setup.py as Python code")
return ''

def extract_version_from_readme_md(self, content: str):
version_from_readme_hint = ''
for line in content.splitlines():
extracted_version = self.extract_version_from_readme_hint(line)
if extracted_version:
if not version_from_readme_hint or (
self.extract_version_from_specifier(extracted_version) > self.extract_version_from_specifier(
version_from_readme_hint)
):
version_from_readme_hint = extracted_version
logger.debug(f"Found Python version hint in README.md: '{extracted_version}'")
return version_from_readme_hint

def determine_python_version(self, git_repo_path: str) -> Optional[str]:
"""
Determines the most specific Python version from project information documents.
Prioritizes pyproject.toml, then setup.py, then README.md.
"""

info_docs_mapping = {PYPROJECT_TOML: self.extract_version_from_pyproject_toml,
SETUP_PY: self.extract_version_from_setup_py,
README_MD: self.extract_version_from_readme_md}

for doc, logic in info_docs_mapping.items():
for root, _, files in os.walk(git_repo_path):
if doc in files:
doc_full_path = os.path.join(root, doc)
with open(doc_full_path, 'r') as file:
python_version = logic(file.read())
if python_version:
return python_version
return '3.11'

def install_dependencies(self, git_repo_path):
cmd = f'cd {git_repo_path} && python -m venv {TRANSITIVE_ENV_NAME}'
run_command(cmd)
with open(git_repo_path / PYTHON_MANIFEST, 'r') as manifest:
for line in tqdm(manifest):
if line.strip() and not PythonLanguageFunctionsParser.is_comment_line(line):
self.install_dependency(line, git_repo_path)

def install_dependency(self, dependency, repo_path):
valid_signs = ['==', '>=', '<=', '!=']
if not any([sign in dependency for sign in valid_signs]):
dependency = dependency.replace('=', '==')
cmd = f'{repo_path}/{TRANSITIVE_ENV_NAME}/bin/python -m pip install {dependency}'
res = run_command(cmd)
if not res:
logger.warning(f'Failed to install dependency {dependency}')



Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TamarW0 You have a duplication of the class PythonDependencyTreeBuilder
Please remove one of them ( make sure that they're identical, if not, keep the one that is the correct one).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TamarW0 I Still see duplication of PythonDependencyTreeBuilder

Comment thread src/vuln_analysis/utils/functions_parsers/lang_functions_parsers.py

@zvigrinberg zvigrinberg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TamarW0 LGTM Approved.
just before Merging, please try to deploy the temp tag on cluster ( with self hosted variant) and verify it works as expected also on cluster.
Also kindly squash all commits into one.
Thank you for your efforts!.

@TamarW0

TamarW0 commented Sep 7, 2025

Copy link
Copy Markdown
Collaborator Author

/retest

@TamarW0 TamarW0 force-pushed the APPENG-2959-migration branch from bd36466 to 2c170a2 Compare September 7, 2025 23:29
@TamarW0 TamarW0 force-pushed the APPENG-2959-migration branch from 1eb8b13 to 72e9859 Compare September 8, 2025 08:10
@TamarW0

TamarW0 commented Sep 8, 2025

Copy link
Copy Markdown
Collaborator Author

/retest

@TamarW0 TamarW0 merged commit 7d3e7ce into rh-aiq-main Sep 8, 2025
heatherzh01 pushed a commit that referenced this pull request Mar 6, 2026
APPENG-2959: Add python transitive support on top of NAT framework
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants