Skip to content

Commit 0d85586

Browse files
committed
Add missing docs
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 27de9f1 commit 0d85586

3 files changed

Lines changed: 202 additions & 49 deletions

File tree

scanpipe/pipelines/analyze_symbols_reachability.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
1-
#
2-
# Copyright (c) nexB Inc. and others. All rights reserved.
3-
# VulnerableCode is a trademark of nexB Inc.
41
# SPDX-License-Identifier: Apache-2.0
5-
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6-
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7-
# See https://aboutcode.org for more information about nexB OSS projects.
82
#
3+
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
22+
923

1024
from scanpipe.pipelines import Pipeline
1125
from scanpipe.pipes import reachability
1226

1327

1428
class SymbolReachability(Pipeline):
15-
"""Patch reachability analysis for given vulnerability patches."""
29+
"""
30+
Patch reachability analysis for given vulnerability patches.
31+
32+
For every patch we clone the git repository and extract
33+
the vulnerable and fixed symbols from the patch commit.
34+
35+
These symbols are then matched against the project's codebase resources to
36+
determine if the vulnerable code is actually present and reachable.
37+
38+
The analysis checks if vulnerable symbols are defined, imported, called, or
39+
exactly match a fingerprint within the project files. The results, including
40+
evidence and a reachability status (REACHABLE, POTENTIALLY_REACHABLE, or
41+
NOT_REACHABLE), are stored in the `extra_data` of the matching resources
42+
under the `symbols_reachability` key.
43+
"""
1644

1745
download_inputs = False
1846
is_addon = True

scanpipe/pipes/reachability.py

Lines changed: 134 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import tempfile
2727
from enum import Enum
2828
from pathlib import Path
29-
from typing import Any
3029

3130
from git import Repo
3231
from git.diff import NULL_TREE
@@ -45,6 +44,7 @@ class ReachabilityStatus(str, Enum):
4544

4645

4746
def normalize_text(content):
47+
"""Normalize content (bytes) into a UTF-8 decoded string."""
4848
if content is None:
4949
return ""
5050

@@ -55,6 +55,10 @@ def normalize_text(content):
5555

5656

5757
def detect_language_with_scancode(file_path, content):
58+
"""
59+
Detect the programming language of the text
60+
content using get_file_info function
61+
"""
5862
content = normalize_text(content)
5963

6064
if not content:
@@ -74,12 +78,12 @@ def detect_language_with_scancode(file_path, content):
7478

7579

7680
class GitRepositoryContext:
77-
def __init__(self, vcs_url: str):
81+
def __init__(self, vcs_url):
7882
self.vcs_url = vcs_url
7983
self.repo_path = None
8084
self._repo = None
8185

82-
def __enter__(self) -> "GitRepositoryContext":
86+
def __enter__(self):
8387
self.repo_path = tempfile.mkdtemp(prefix="symbol-reachability-")
8488
try:
8589
self._repo = Repo.clone_from(self.vcs_url, self.repo_path)
@@ -101,12 +105,25 @@ def _cleanup(self):
101105

102106

103107
class PatchAnalyzer:
104-
def __init__(self, repo: Repo, commit_hash: str):
108+
def __init__(self, repo: Repo, commit_hash):
105109
self.repo = repo
106110
self.commit = repo.commit(commit_hash)
107111
self.parent_commit = self.commit.parents[0] if self.commit.parents else None
108112

109113
def get_changed_files(self):
114+
"""
115+
Retrieve all files changed by the commit along with their
116+
vulnerable and fixed contents.
117+
118+
For each changed file, a dictionary entry is created with two
119+
keys:
120+
121+
- vulnerable_text: The file content before the commit (empty
122+
string for newly added files).
123+
- fixed_text: The file content after the commit (empty string
124+
for deleted files).
125+
126+
"""
110127
diffs = (
111128
self.parent_commit.diff(self.commit, create_patch=False)
112129
if self.parent_commit
@@ -170,6 +187,15 @@ def compute_changed_lines(cls, vulnerable_text, fixed_text):
170187

171188
@classmethod
172189
def diff_changed_symbols(cls, vuln_meta, fixed_meta):
190+
"""
191+
Compare the vulnerable and fixed symbol metadata and return the
192+
symbols that are unique to each side (i.e., whose body text
193+
differs between the two versions).
194+
195+
A symbol key is considered "vulnerable-only" if its body text
196+
does not match the corresponding symbol in fixed_meta, and
197+
vice versa.
198+
"""
173199
vuln_only = {
174200
key: metadata
175201
for key, metadata in vuln_meta.items()
@@ -183,6 +209,19 @@ def diff_changed_symbols(cls, vuln_meta, fixed_meta):
183209
return vuln_only, fixed_only
184210

185211
def collect_patch_symbols(self):
212+
"""
213+
Collect all changed symbols across every file modified by the
214+
commit, grouped by programming language.
215+
216+
For each changed file, the analyzer:
217+
- Retrieves the vulnerable and fixed file contents.
218+
- Computes which lines were removed and added.
219+
- Extracts symbols that intersect those changed lines using
220+
Tree-sitter parsing SymbolExtractor
221+
- Diffs the extracted symbols to find those whose body text
222+
actually changed.
223+
- Buckets the results by programming language.
224+
"""
186225
by_language = {}
187226
changed_files = self.get_changed_files()
188227

@@ -221,9 +260,16 @@ def collect_patch_symbols(self):
221260
return by_language
222261

223262
@classmethod
224-
def build_symbol_metadata(
225-
cls, nodes, extractor: SymbolExtractor, index: dict = None
226-
):
263+
def build_symbol_metadata(cls, nodes, extractor: SymbolExtractor, index):
264+
"""
265+
Build metadata dictionaries for a list of Tree-sitter AST nodes
266+
representing changed symbols.
267+
268+
For each node, the qualified name, body text, SHA-256 fingerprint,
269+
and start/end line numbers are extracted and stored in a
270+
dictionary keyed by the qualified name. If duplicate qualified
271+
names are encountered, a numeric suffix is appended to disambiguate.
272+
"""
227273
if not nodes or not extractor:
228274
return {}
229275

@@ -259,6 +305,23 @@ def build_symbol_metadata(
259305
def analyze(
260306
cls, vulnerable_text, fixed_text, removed_lines, added_lines, file_path
261307
):
308+
"""
309+
Analyze the vulnerable and fixed versions of a single file to
310+
extract changed symbols.
311+
312+
The method performs the following steps:
313+
314+
- Detects the programming language of the file (using the
315+
fixed version first, falling back to the vulnerable version).
316+
- Verifies the language is supported by Tree-sitter queries.
317+
- Parses both versions into ASTs using Tree-sitter.
318+
- Extracts symbols whose line ranges intersect the removed
319+
lines (vulnerable side) or added lines (fixed side).
320+
- Builds metadata for each set of changed symbols.
321+
- Diffs the two metadata sets to find symbols whose body text
322+
actually changed between versions.
323+
324+
"""
262325
vulnerable_text = normalize_text(vulnerable_text)
263326
fixed_text = normalize_text(fixed_text)
264327

@@ -317,6 +380,23 @@ def analyze(
317380

318381

319382
def generate_reachability_report(patch, repo, candidate_resources, logger=None):
383+
"""
384+
Generate and store a symbol reachability report for a single
385+
vulnerability patch against a set of candidate codebase resources.
386+
387+
- Creates a PatchAnalyzer for the given commit.
388+
- Collects changed symbols from the patch, grouped by language.
389+
- For each candidate resource whose language matches a patch
390+
language, builds a ResourceAnalyzer index and uses
391+
ResourcePatchMatcher to match vulnerable and fixed
392+
symbols.
393+
- If matches are found, constructs a report dict containing
394+
evidence, matched symbol names, and a ReachabilityStatus.
395+
- Appends the report to the resource's extra_data under the
396+
symbols_reachability key (avoiding duplicates for the same
397+
commit hash).
398+
399+
"""
320400
vcs_url = patch.get("vcs_url")
321401
commit_hash = patch.get("commit_hash")
322402

@@ -384,6 +464,20 @@ def generate_reachability_report(patch, repo, candidate_resources, logger=None):
384464

385465

386466
def analyze_and_store_symbol_reachability_results(project, logger=None):
467+
"""
468+
Analyze all vulnerability patches for a project and store the
469+
resulting symbol reachability reports
470+
471+
The function iterates over all package vulnerabilities associated
472+
with the project, groups their patches by repository URL, clones
473+
each repository (using :class:`GitRepositoryContext`), checks out
474+
each patch's commit, and calls :func:`generate_reachability_report`
475+
to determine whether the vulnerable/fixed symbols from the patch
476+
are reachable in the project's codebase resources.
477+
478+
Only non-binary, non-archive, non-media files are considered as
479+
candidate resources.
480+
"""
387481
candidate_resources = project.codebaseresources.files().filter(
388482
is_binary=False, is_archive=False, is_media=False
389483
)
@@ -414,6 +508,10 @@ def analyze_and_store_symbol_reachability_results(project, logger=None):
414508

415509

416510
def classify_reachability(evidence):
511+
"""
512+
Classify the reachability status of a vulnerability based on the
513+
collected evidence from :class:`ResourcePatchMatcher`.
514+
"""
417515
if not evidence:
418516
return ReachabilityStatus.NOT_REACHABLE
419517

@@ -435,14 +533,18 @@ def classify_reachability(evidence):
435533

436534

437535
class ResourceAnalyzer:
438-
def __init__(self, resource_text: str, language: str):
536+
def __init__(self, resource_text, language):
439537
self.resource_text = normalize_text(resource_text)
440538
self.language = language
441539

442540
def process_node(
443-
self, node, extractor, definitions_index, definitions: set, fingerprints: set
444-
) -> str | None:
445-
"""Extract the qualified name, update definitions, and fingerprint"""
541+
self, node, extractor, definitions_index, definitions, fingerprints
542+
):
543+
"""
544+
Process a single AST node to extract its qualified name, add it
545+
to the definitions set, compute its fingerprint, and add the
546+
fingerprint to the fingerprints set.
547+
"""
446548
qualified_name = extractor._build_qualified_name(node, definitions_index)
447549
if not qualified_name:
448550
return None
@@ -456,7 +558,16 @@ def process_node(
456558

457559
return qualified_name
458560

459-
def build_index(self) -> dict | None:
561+
def build_index(self):
562+
"""
563+
Build the full symbol index for the resource by parsing it
564+
with Tree-sitter and extracting all definitions, fingerprints,
565+
imports, and the reverse call graph.
566+
567+
The method iterates over all functions, classes, and constants
568+
in the resource's AST. For functions, it also extracts call
569+
expressions to populate the callers_of reverse call graph.
570+
"""
460571
if not is_supported_language(self.language) or not self.resource_text:
461572
return None
462573

@@ -503,15 +614,24 @@ def build_index(self) -> dict | None:
503614

504615

505616
class ResourcePatchMatcher:
506-
def __init__(self, resource_index: dict):
617+
def __init__(self, resource_index):
507618
self.resource_index = resource_index
508619
self.definitions = resource_index.get("definitions", set())
509620
self.fingerprints = resource_index.get("fingerprints", set())
510621
self.imports = resource_index.get("imports", {})
511622
self.callers_of = resource_index.get("callers_of", {})
512623
self.separator = resource_index.get("separator", ".")
513624

514-
def match(self, patch_symbols_metadata: dict[str, Any]) -> dict[str, Any]:
625+
def match(self, patch_symbols_metadata):
626+
"""
627+
Match a set of patch symbols against the resource index and
628+
return evidence for each matched symbol.
629+
630+
For each symbol in patch_symbols_metadata, the method
631+
checks whether it is defined, imported, called, or has an
632+
exact fingerprint match in the resource. If at least one of
633+
these conditions is true, an evidence entry is created.
634+
"""
515635
if not patch_symbols_metadata or not self.resource_index:
516636
return {}
517637

0 commit comments

Comments
 (0)