From e818133940d963f26fc5c47dc51e90be614944bc Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Thu, 29 May 2025 15:17:34 -0700 Subject: [PATCH 01/18] Update: added exported function scanning in Plugin --- plugins/angrimportfinder/README.md | 7 ++++--- .../surfactantplugin_angrimportfinder.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/plugins/angrimportfinder/README.md b/plugins/angrimportfinder/README.md index cde90afaf..d2ca9c600 100644 --- a/plugins/angrimportfinder/README.md +++ b/plugins/angrimportfinder/README.md @@ -1,4 +1,4 @@ -# Imported function name extractor Plugin for SBOM Surfactant +# Imported and Exported function name extractor Plugin for SBOM Surfactant A plugin for Surfactant that uses the [angr](https://github.com/angr/angr) Python library to extract the imported function names from ELF and PE files. @@ -10,7 +10,7 @@ In the same virtual environment that Surfactant was installed in, install this p For developers making changes to this plugin, install it with `pip install -e .`. After installing the plugin, run Surfactant to generate an SBOM as usual and entries for ELF -and PE files will generate additional json files in the working directory that contain the list of imported functions of the executable files. +and PE files will generate additional json files in the working directory that contain the list of functions of the executable files. If there are duplicate hashed files the extractor will skip the entry. Example: @@ -20,7 +20,8 @@ Example: { "sha256hash": "", "filename": [], - "imported function names": [] + "imported function names": [], + "exported function names": [] } ``` diff --git a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py index 4112ea87c..a36b3658a 100644 --- a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py +++ b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py @@ -43,24 +43,27 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s if existing_json: with open(existing_json, "r") as json_file: existing_data = json.load(json_file) - if "imported function names" in existing_data: + if "imported function names" in existing_data and "exported function names" in existing_data: logger.info(f"Already extracted {filename.name}") else: try: logger.info( - f"Found existing JSON file for {filename.name} but without 'imported functions' key. Proceeding with extraction." + f"Found existing JSON file for {filename.name} but without 'imported function names' or 'exported function names' keys. Proceeding with extraction." ) # Add your extraction code here. if filename.name not in existing_data["filename"]: existing_data["filename"].append(filename.name) existing_data["imported function names"] = [] + existing_data["exported function names"] = [] # Create an angr project project = angr.Project(filename, auto_load_libs=False) # Get the imported functions using symbol information for symbol in project.loader.main_object.symbols: - if symbol.is_function: - existing_data["imported function names"].append(symbol.name) + if symbol.is_import: + metadata["imported function names"].append(symbol.name) + elif symbol.is_export: + metadata["exported function names"].append(symbol.name) # Write the string_dict to the output JSON file with open(output_name, "w") as json_file: @@ -79,12 +82,15 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s metadata["sha256hash"] = filehash metadata["filename"] = [filename.name] metadata["imported function names"] = [] + metadata["exported function names"] = [] # Create an angr project project = angr.Project(filename.as_posix(), auto_load_libs=False) - # Get the imported functions using symbol information + # Get the functions using symbol information for symbol in project.loader.main_object.symbols: - if symbol.is_function: + if symbol.is_import: metadata["imported function names"].append(symbol.name) + elif symbol.is_export: + metadata["exported function names"].append(symbol.name) # Write the string_dict to the output JSON file with open(output_path, "w") as json_file: From 3164728554070b0e8ab2b5212cc1f7b13d34ed78 Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Fri, 6 Jun 2025 09:50:50 -0700 Subject: [PATCH 02/18] export to import function reachability plugin --- plugins/reachability/README.md | 40 +++++ plugins/reachability/pyproject.toml | 34 ++++ .../surfactantplugin_reachability.py | 155 ++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 plugins/reachability/README.md create mode 100644 plugins/reachability/pyproject.toml create mode 100644 plugins/reachability/surfactantplugin_reachability.py diff --git a/plugins/reachability/README.md b/plugins/reachability/README.md new file mode 100644 index 000000000..f7e504080 --- /dev/null +++ b/plugins/reachability/README.md @@ -0,0 +1,40 @@ +# Import Reachability Plugin for SBOM Surfactant + +A plugin for Surfactant that checks the reachability of imported functions within exported functions to narrow down reachable code and reduse the amount of deadcode analysts are having to view. + +## Quickstart + +To install this plugin within the same virtual environment as Surfactant, use the command `pip install .`. + +For developers modifying the plugin, the editable installation can be achieved with `pip install -e .`. + +After installing the plugin, run Surfactant to generate an SBOM as usual and entries for ELF +and PE files will contain a metadata object with the information that checksec.py was able +to get about security related features. + +After the plugin installation, run Surfactant as you normally would to create an SBOM. For binary files analyzed by this plugin, additional JSON files will be generated containing vulnerability data extracted from the binaries. If there are duplicate hashed files, the extractor will check if they have the exported functions entries and skip remaking the output file if so. + +Example: +Output Filename: `$(sha256hash)_additional_metadata.json` + +```json +{ + "sha256hash": " ", + "filename": [], + "exported function dependencies": { + "exported function 1": [], + "exported function 2": [] + } +} +``` + +The plugin's functionality can be toggled via Surfactant's plugin management features, using the plugin name `surfactantplugin_reachability.py` as defined in the `pyproject.toml` under the `project.entry-points."surfactant"` section. + +## Uninstalling + +Remove the plugin from your environment with `pip uninstall surfactantplugin_reachability`. + +## Important Licensing Information +Main Project License (Surfactant): MIT License. + +Plugin License: MIT License, but it includes and uses cve-bin-tool, which is GPL-3.0 licensed. diff --git a/plugins/reachability/pyproject.toml b/plugins/reachability/pyproject.toml new file mode 100644 index 000000000..59db21999 --- /dev/null +++ b/plugins/reachability/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "surfactantplugin_reachability" +authors = [ + {name = "Seth Bredbenner", email = "bredbenner1@llnl.gov"}, +] +description = "Surfactant plugin for running grype on files" +readme = "README.md" +requires-python = ">=3.8" +keywords = ["surfactant"] +license = {text = "MIT License"} +classifiers = [ + "Programming Language :: Python :: 3", + "Environment :: Console", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "angr", + "surfactant", +] +dynamic = ["version"] + +[project.entry-points."surfactant"] +"surfactantplugin_reachability" = "surfactantplugin_reachability" + +[tool.setuptools] +py-modules=["surfactantplugin_reachability"] + diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py new file mode 100644 index 000000000..b1b1f36ed --- /dev/null +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -0,0 +1,155 @@ +# Copyright 2023 Lawrence Livermore National Security, LLC +# See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: MIT +import json +from pathlib import Path + +import angr +from cle import CLECompatibilityError +from loguru import logger + +import surfactant.plugin +from surfactant.sbomtypes import SBOM, Software + + +@surfactant.plugin.hookimpl(specname="extract_file_info") +# extract_strings(sbom: SBOM, software: Software, filename: str, filetype: str): +# def angrimport_finder(filename: str, filetype: str, filehash: str): +def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: str): + """ + :param sbom(SBOM): The SBOM that the software entry/file is being added to. Can be used to add observations or analysis data. + :param software(Software): The software entry associated with the file to extract information from. + :param filename (str): The full path to the file to extract information from. + :param filetype (str): File type information based on magic bytes. + """ + + # Only parsing executable files + if filetype not in ["ELF", "PE"]: + pass + filehash = str(software.sha256) + filename = Path(filename) + flist = [] + + # Performing check to see if file has been analyzed already + existing_json = None + output_name = None + for f in Path.cwd().glob("*_additional_metadata.json"): + flist.append((f.stem).split("_")[0]) + if filehash == (f.stem).split("_")[0]: + existing_json = f + output_name = f + + if existing_json: + with open(existing_json, "r") as json_file: + existing_data = json.load(json_file) + if "exported function dependencies" in existing_data: + logger.info(f"Already extracted {filename.name}") + else: + try: + logger.info( + f"Found existing JSON file for {filename.name} but without 'exported function dependencies' keys. Proceeding with extraction." + ) + # Add your extraction code here. + if filename.name not in existing_data["filename"]: + existing_data["filename"].append(filename.name) + existing_data["exported function dependencies"] = {} + + # Create an angr project + project = angr.Project(filename, load_options = {'auto_load_libs': True}) + + #library dependencies {import: library} + lookup = {} + for obj in project.loader.main_object.imports.keys(): + lookup[obj] = project.loader.find_symbol(obj).owner.provides + + #recreates our angr project without the libraries loaded to save on time + project = angr.Project(filename, load_options = {'auto_load_libs': False}) + + #holds our data for our JSON file + database = existing_data["exported function dependencies"] + + cfg = project.analyses.CFGFast() + + #holds every export address error is here + exports = [func.rebased_addr for func in project.loader.main_object.symbols if func.is_export] #_exports is only available for PE files + + #go through every exported function + for exp_addr in exports: + + exp_name = project.kb.functions[exp_addr].name + database[exp_name] = [] + + #goes through every function that is reachable from exported function + for imported_address in cfg.functions.callgraph.successors(exp_addr): + imported_function = cfg.functions.get(imported_address) + + #checks if the function is imported + if imported_function.name in project.loader.main_object.imports.keys(): + + library = lookup[imported_function.name] + + #adds our entry in the form of list[library, imported_function] + database[exp_name].append([library, imported_function.name]) + + # Write the string_dict to the output JSON file + with open(output_name, "w") as json_file: + json.dump(existing_data, json_file, indent=4) + except CLECompatibilityError as e: + logger.info(f"Angr Error {filename} {e}") + else: + try: + # Validate the file path + if not filename.exists(): + raise FileNotFoundError(f"No such file: '{filename}'") + + # Extract filename without extension + output_path = Path.cwd() / f"{filehash}_additional_metadata.json" + metadata = {} + metadata["sha256hash"] = filehash + metadata["filename"] = [filename.name] + metadata['exported function dependencies'] = {} + # Create an angr project + project = angr.Project(filename, load_options = {'auto_load_libs': True}) + + #library dependencies {import: library} + lookup = {} + for obj in project.loader.main_object.imports.keys(): + lookup[obj] = project.loader.find_symbol(obj).owner.provides + + #recreates our angr project without the libraries loaded to save on time + project = angr.Project(filename, load_options = {'auto_load_libs': False}) + + #holds our data for our JSON file + database = metadata["exported function dependencies"] + + cfg = project.analyses.CFGFast() + + #holds every export address error is here + exports = [func.rebased_addr for func in project.loader.main_object.symbols if func.is_export] #_exports is only available for PE files + + #go through every exported function + for exp_addr in exports: + + exp_name = project.kb.functions[exp_addr].name + database[exp_name] = [] + + #goes through every function that is reachable from exported function + for imported_address in cfg.functions.callgraph.successors(exp_addr): + imported_function = cfg.functions.get(imported_address) + + #checks if the function is imported + if imported_function.name in project.loader.main_object.imports.keys(): + + library = lookup[imported_function.name] + + #adds our entry in the form of list[library, imported_function] + database[exp_name].append([library, imported_function.name]) + + # Write the string_dict to the output JSON file + with open(output_path, "w") as json_file: + json.dump(metadata, json_file, indent=4) + + logger.info(f"Data written to {output_path}") + except CLECompatibilityError as e: + logger.info(f"Angr Error {filename} {e}") From 694d1dc49f878e32d4e5813ba1cda7f86cf7dfa9 Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Fri, 6 Jun 2025 09:59:02 -0700 Subject: [PATCH 03/18] Fixed an issue within angrimportfinder --- plugins/angrimportfinder/surfactantplugin_angrimportfinder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py index a36b3658a..3caf8ce65 100644 --- a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py +++ b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py @@ -61,9 +61,9 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s # Get the imported functions using symbol information for symbol in project.loader.main_object.symbols: if symbol.is_import: - metadata["imported function names"].append(symbol.name) + existing_data["imported function names"].append(symbol.name) elif symbol.is_export: - metadata["exported function names"].append(symbol.name) + existing_data["exported function names"].append(symbol.name) # Write the string_dict to the output JSON file with open(output_name, "w") as json_file: From 694e1002a4355bbcf65a29ec985df8ae10b0e64c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:01:09 +0000 Subject: [PATCH 04/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../surfactantplugin_angrimportfinder.py | 5 +- plugins/reachability/pyproject.toml | 1 - .../surfactantplugin_reachability.py | 56 ++++++++++--------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py index 3caf8ce65..db0f01bc3 100644 --- a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py +++ b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py @@ -43,7 +43,10 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s if existing_json: with open(existing_json, "r") as json_file: existing_data = json.load(json_file) - if "imported function names" in existing_data and "exported function names" in existing_data: + if ( + "imported function names" in existing_data + and "exported function names" in existing_data + ): logger.info(f"Already extracted {filename.name}") else: try: diff --git a/plugins/reachability/pyproject.toml b/plugins/reachability/pyproject.toml index 59db21999..9561380a7 100644 --- a/plugins/reachability/pyproject.toml +++ b/plugins/reachability/pyproject.toml @@ -31,4 +31,3 @@ dynamic = ["version"] [tool.setuptools] py-modules=["surfactantplugin_reachability"] - diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index b1b1f36ed..9344e1e2b 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -56,40 +56,42 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s existing_data["exported function dependencies"] = {} # Create an angr project - project = angr.Project(filename, load_options = {'auto_load_libs': True}) + project = angr.Project(filename, load_options={"auto_load_libs": True}) - #library dependencies {import: library} + # library dependencies {import: library} lookup = {} for obj in project.loader.main_object.imports.keys(): lookup[obj] = project.loader.find_symbol(obj).owner.provides - #recreates our angr project without the libraries loaded to save on time - project = angr.Project(filename, load_options = {'auto_load_libs': False}) + # recreates our angr project without the libraries loaded to save on time + project = angr.Project(filename, load_options={"auto_load_libs": False}) - #holds our data for our JSON file + # holds our data for our JSON file database = existing_data["exported function dependencies"] cfg = project.analyses.CFGFast() - #holds every export address error is here - exports = [func.rebased_addr for func in project.loader.main_object.symbols if func.is_export] #_exports is only available for PE files + # holds every export address error is here + exports = [ + func.rebased_addr + for func in project.loader.main_object.symbols + if func.is_export + ] # _exports is only available for PE files - #go through every exported function + # go through every exported function for exp_addr in exports: - exp_name = project.kb.functions[exp_addr].name database[exp_name] = [] - #goes through every function that is reachable from exported function + # goes through every function that is reachable from exported function for imported_address in cfg.functions.callgraph.successors(exp_addr): imported_function = cfg.functions.get(imported_address) - #checks if the function is imported + # checks if the function is imported if imported_function.name in project.loader.main_object.imports.keys(): - library = lookup[imported_function.name] - #adds our entry in the form of list[library, imported_function] + # adds our entry in the form of list[library, imported_function] database[exp_name].append([library, imported_function.name]) # Write the string_dict to the output JSON file @@ -108,42 +110,42 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s metadata = {} metadata["sha256hash"] = filehash metadata["filename"] = [filename.name] - metadata['exported function dependencies'] = {} + metadata["exported function dependencies"] = {} # Create an angr project - project = angr.Project(filename, load_options = {'auto_load_libs': True}) + project = angr.Project(filename, load_options={"auto_load_libs": True}) - #library dependencies {import: library} + # library dependencies {import: library} lookup = {} for obj in project.loader.main_object.imports.keys(): lookup[obj] = project.loader.find_symbol(obj).owner.provides - #recreates our angr project without the libraries loaded to save on time - project = angr.Project(filename, load_options = {'auto_load_libs': False}) + # recreates our angr project without the libraries loaded to save on time + project = angr.Project(filename, load_options={"auto_load_libs": False}) - #holds our data for our JSON file + # holds our data for our JSON file database = metadata["exported function dependencies"] cfg = project.analyses.CFGFast() - #holds every export address error is here - exports = [func.rebased_addr for func in project.loader.main_object.symbols if func.is_export] #_exports is only available for PE files + # holds every export address error is here + exports = [ + func.rebased_addr for func in project.loader.main_object.symbols if func.is_export + ] # _exports is only available for PE files - #go through every exported function + # go through every exported function for exp_addr in exports: - exp_name = project.kb.functions[exp_addr].name database[exp_name] = [] - #goes through every function that is reachable from exported function + # goes through every function that is reachable from exported function for imported_address in cfg.functions.callgraph.successors(exp_addr): imported_function = cfg.functions.get(imported_address) - #checks if the function is imported + # checks if the function is imported if imported_function.name in project.loader.main_object.imports.keys(): - library = lookup[imported_function.name] - #adds our entry in the form of list[library, imported_function] + # adds our entry in the form of list[library, imported_function] database[exp_name].append([library, imported_function.name]) # Write the string_dict to the output JSON file From 7e8a80e3d1604c9702f21ed61d1086fefed1556f Mon Sep 17 00:00:00 2001 From: TopAce <144856581+T0pAc3@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:09:36 -0700 Subject: [PATCH 05/18] Update README.md --- plugins/reachability/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/reachability/README.md b/plugins/reachability/README.md index f7e504080..2e2bcbe7e 100644 --- a/plugins/reachability/README.md +++ b/plugins/reachability/README.md @@ -19,11 +19,11 @@ Output Filename: `$(sha256hash)_additional_metadata.json` ```json { - "sha256hash": " ", + "sha256hash": "", "filename": [], "exported function dependencies": { - "exported function 1": [], - "exported function 2": [] + ["library", "exported_function1"], + ["library", "exported_function2"] } } ``` From b2685511b7d980dfba507e58bb0e0c51a91583a7 Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Fri, 6 Jun 2025 09:59:02 -0700 Subject: [PATCH 06/18] Fixed an issue within angrimportfinder --- plugins/angrimportfinder/surfactantplugin_angrimportfinder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py index a36b3658a..3caf8ce65 100644 --- a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py +++ b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py @@ -61,9 +61,9 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s # Get the imported functions using symbol information for symbol in project.loader.main_object.symbols: if symbol.is_import: - metadata["imported function names"].append(symbol.name) + existing_data["imported function names"].append(symbol.name) elif symbol.is_export: - metadata["exported function names"].append(symbol.name) + existing_data["exported function names"].append(symbol.name) # Write the string_dict to the output JSON file with open(output_name, "w") as json_file: From 2d5730c835f4c5c7aeb63496c6d8f61d8b288626 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:09:37 +0000 Subject: [PATCH 07/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../angrimportfinder/surfactantplugin_angrimportfinder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py index 3caf8ce65..db0f01bc3 100644 --- a/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py +++ b/plugins/angrimportfinder/surfactantplugin_angrimportfinder.py @@ -43,7 +43,10 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s if existing_json: with open(existing_json, "r") as json_file: existing_data = json.load(json_file) - if "imported function names" in existing_data and "exported function names" in existing_data: + if ( + "imported function names" in existing_data + and "exported function names" in existing_data + ): logger.info(f"Already extracted {filename.name}") else: try: From 42ce1235ea203b1951bb27d547577068b0685db7 Mon Sep 17 00:00:00 2001 From: TopAce <144856581+T0pAc3@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:54:23 -0700 Subject: [PATCH 08/18] Update plugins/angrimportfinder/README.md Co-authored-by: Ryan Mast <3969255+nightlark@users.noreply.github.com> --- plugins/angrimportfinder/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/angrimportfinder/README.md b/plugins/angrimportfinder/README.md index d2ca9c600..078853932 100644 --- a/plugins/angrimportfinder/README.md +++ b/plugins/angrimportfinder/README.md @@ -10,7 +10,7 @@ In the same virtual environment that Surfactant was installed in, install this p For developers making changes to this plugin, install it with `pip install -e .`. After installing the plugin, run Surfactant to generate an SBOM as usual and entries for ELF -and PE files will generate additional json files in the working directory that contain the list of functions of the executable files. +and PE files will generate additional json files in the working directory that contain the list of imported and exported functions in the executable files. If there are duplicate hashed files the extractor will skip the entry. Example: From a6a3a2f5f84eccc39201744f1a8f59282a01084f Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Thu, 12 Jun 2025 14:30:10 -0700 Subject: [PATCH 09/18] surfactant reachability plugin --- plugins/reachability/README.md | 14 +- .../surfactantplugin_reachability.py | 191 ++++++------------ 2 files changed, 70 insertions(+), 135 deletions(-) diff --git a/plugins/reachability/README.md b/plugins/reachability/README.md index 2e2bcbe7e..30be63c33 100644 --- a/plugins/reachability/README.md +++ b/plugins/reachability/README.md @@ -15,15 +15,17 @@ to get about security related features. After the plugin installation, run Surfactant as you normally would to create an SBOM. For binary files analyzed by this plugin, additional JSON files will be generated containing vulnerability data extracted from the binaries. If there are duplicate hashed files, the extractor will check if they have the exported functions entries and skip remaking the output file if so. Example: -Output Filename: `$(sha256hash)_additional_metadata.json` +Output Filename: `reachability.json` ```json { - "sha256hash": "", - "filename": [], - "exported function dependencies": { - ["library", "exported_function1"], - ["library", "exported_function2"] + "filename": { + "exp_func": { + "library": [ + "imp_func1", + "imp_func2" + ] + } } } ``` diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index 9344e1e2b..abc7965b7 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -10,13 +10,11 @@ from loguru import logger import surfactant.plugin -from surfactant.sbomtypes import SBOM, Software - @surfactant.plugin.hookimpl(specname="extract_file_info") # extract_strings(sbom: SBOM, software: Software, filename: str, filetype: str): # def angrimport_finder(filename: str, filetype: str, filehash: str): -def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: str): +def reachability(filename: str, filetype: str): """ :param sbom(SBOM): The SBOM that the software entry/file is being added to. Can be used to add observations or analysis data. :param software(Software): The software entry associated with the file to extract information from. @@ -27,131 +25,66 @@ def angrimport_finder(sbom: SBOM, software: Software, filename: str, filetype: s # Only parsing executable files if filetype not in ["ELF", "PE"]: pass - filehash = str(software.sha256) filename = Path(filename) - flist = [] - - # Performing check to see if file has been analyzed already - existing_json = None - output_name = None - for f in Path.cwd().glob("*_additional_metadata.json"): - flist.append((f.stem).split("_")[0]) - if filehash == (f.stem).split("_")[0]: - existing_json = f - output_name = f - - if existing_json: - with open(existing_json, "r") as json_file: - existing_data = json.load(json_file) - if "exported function dependencies" in existing_data: - logger.info(f"Already extracted {filename.name}") - else: - try: - logger.info( - f"Found existing JSON file for {filename.name} but without 'exported function dependencies' keys. Proceeding with extraction." - ) - # Add your extraction code here. - if filename.name not in existing_data["filename"]: - existing_data["filename"].append(filename.name) - existing_data["exported function dependencies"] = {} - - # Create an angr project - project = angr.Project(filename, load_options={"auto_load_libs": True}) - - # library dependencies {import: library} - lookup = {} - for obj in project.loader.main_object.imports.keys(): - lookup[obj] = project.loader.find_symbol(obj).owner.provides - - # recreates our angr project without the libraries loaded to save on time - project = angr.Project(filename, load_options={"auto_load_libs": False}) - - # holds our data for our JSON file - database = existing_data["exported function dependencies"] - - cfg = project.analyses.CFGFast() - - # holds every export address error is here - exports = [ - func.rebased_addr - for func in project.loader.main_object.symbols - if func.is_export - ] # _exports is only available for PE files - - # go through every exported function - for exp_addr in exports: - exp_name = project.kb.functions[exp_addr].name - database[exp_name] = [] - - # goes through every function that is reachable from exported function - for imported_address in cfg.functions.callgraph.successors(exp_addr): - imported_function = cfg.functions.get(imported_address) - - # checks if the function is imported - if imported_function.name in project.loader.main_object.imports.keys(): - library = lookup[imported_function.name] - - # adds our entry in the form of list[library, imported_function] - database[exp_name].append([library, imported_function.name]) - - # Write the string_dict to the output JSON file - with open(output_name, "w") as json_file: - json.dump(existing_data, json_file, indent=4) - except CLECompatibilityError as e: - logger.info(f"Angr Error {filename} {e}") + + # Performing check to see if the file exists or not + output_path = Path.cwd() / "reachability.json" + + if output_path.exists(): + with open(output_path, "r") as json_file: + database = json.load(json_file) else: - try: - # Validate the file path - if not filename.exists(): - raise FileNotFoundError(f"No such file: '{filename}'") - - # Extract filename without extension - output_path = Path.cwd() / f"{filehash}_additional_metadata.json" - metadata = {} - metadata["sha256hash"] = filehash - metadata["filename"] = [filename.name] - metadata["exported function dependencies"] = {} - # Create an angr project - project = angr.Project(filename, load_options={"auto_load_libs": True}) - - # library dependencies {import: library} - lookup = {} - for obj in project.loader.main_object.imports.keys(): - lookup[obj] = project.loader.find_symbol(obj).owner.provides - - # recreates our angr project without the libraries loaded to save on time - project = angr.Project(filename, load_options={"auto_load_libs": False}) - - # holds our data for our JSON file - database = metadata["exported function dependencies"] - - cfg = project.analyses.CFGFast() - - # holds every export address error is here - exports = [ - func.rebased_addr for func in project.loader.main_object.symbols if func.is_export - ] # _exports is only available for PE files - - # go through every exported function - for exp_addr in exports: - exp_name = project.kb.functions[exp_addr].name - database[exp_name] = [] - - # goes through every function that is reachable from exported function - for imported_address in cfg.functions.callgraph.successors(exp_addr): - imported_function = cfg.functions.get(imported_address) - - # checks if the function is imported - if imported_function.name in project.loader.main_object.imports.keys(): - library = lookup[imported_function.name] - - # adds our entry in the form of list[library, imported_function] - database[exp_name].append([library, imported_function.name]) - - # Write the string_dict to the output JSON file - with open(output_path, "w") as json_file: - json.dump(metadata, json_file, indent=4) - - logger.info(f"Data written to {output_path}") - except CLECompatibilityError as e: - logger.info(f"Angr Error {filename} {e}") + database = {} + + try: + if not filename.exists(): + raise FileNotFoundError(f"No such file: '{filename}'") + # Add your extraction code here. + # Create an angr project + project = angr.Project(filename, load_options={"auto_load_libs": True}) + + # library dependencies {import: library} + lookup = {} + for obj in project.loader.main_object.imports.keys(): + library = project.loader.find_symbol(obj).owner.provides + lookup[obj] = library + + # recreates our angr project without the libraries loaded to save on time + project = angr.Project(filename, load_options={"auto_load_libs": False}) + + cfg = project.analyses.CFGFast() + + # holds every export address error is here + exports = [ + func.rebased_addr + for func in project.loader.main_object.symbols + if func.is_export + ] # _exports is only available for PE files + database[filename.name] = {} + + # go through every exported function + for exp_addr in exports: + exp_name = cfg.functions.get(exp_addr).name + database[filename.name][exp_name] = {} + + # goes through every function that is reachable from exported function + for imported_address in cfg.functions.callgraph.successors(exp_addr): + imported_function = cfg.functions.get(imported_address) + + # checks if the function is imported + if imported_function.name in project.loader.main_object.imports.keys(): + library = lookup[imported_function.name] + + if library not in database[filename.name][exp_name].keys(): + database[filename.name][exp_name][library] = [] + + # adds our reachable imported function as a dependency + if imported_function.name not in database[filename.name][exp_name][library]: + database[filename.name][exp_name][library].append(imported_function.name) + + # Write the string_dict to the output JSON file + with open(output_path, "w") as json_file: + json.dump(database, json_file, indent=4) + + except CLECompatibilityError as e: + logger.info(f"Angr Error {filename} {e}") \ No newline at end of file From 9dab6b4e9ddb7e361313319d450571798143c67c Mon Sep 17 00:00:00 2001 From: TopAce <144856581+T0pAc3@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:44:48 -0700 Subject: [PATCH 10/18] Update README.md --- plugins/angrimportfinder/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/angrimportfinder/README.md b/plugins/angrimportfinder/README.md index d2ca9c600..98cee7e71 100644 --- a/plugins/angrimportfinder/README.md +++ b/plugins/angrimportfinder/README.md @@ -1,7 +1,7 @@ # Imported and Exported function name extractor Plugin for SBOM Surfactant A plugin for Surfactant that uses the [angr](https://github.com/angr/angr) -Python library to extract the imported function names from ELF and PE files. +Python library to extract the imported and exported function names from ELF and PE files. ## Quickstart From f0a75930f4fe7bea5dd5fa4e6dc0742e4e8568f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:56:44 +0000 Subject: [PATCH 11/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/reachability/surfactantplugin_reachability.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index abc7965b7..223764c34 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -11,6 +11,7 @@ import surfactant.plugin + @surfactant.plugin.hookimpl(specname="extract_file_info") # extract_strings(sbom: SBOM, software: Software, filename: str, filetype: str): # def angrimport_finder(filename: str, filetype: str, filehash: str): @@ -56,9 +57,7 @@ def reachability(filename: str, filetype: str): # holds every export address error is here exports = [ - func.rebased_addr - for func in project.loader.main_object.symbols - if func.is_export + func.rebased_addr for func in project.loader.main_object.symbols if func.is_export ] # _exports is only available for PE files database[filename.name] = {} @@ -87,4 +86,4 @@ def reachability(filename: str, filetype: str): json.dump(database, json_file, indent=4) except CLECompatibilityError as e: - logger.info(f"Angr Error {filename} {e}") \ No newline at end of file + logger.info(f"Angr Error {filename} {e}") From 26f478ac2caaa85be88c25c74c69c893ebcf077a Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Thu, 12 Jun 2025 15:49:21 -0700 Subject: [PATCH 12/18] turned JSON form into python dictionary --- .../surfactantplugin_reachability.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index abc7965b7..6869ff835 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -27,14 +27,7 @@ def reachability(filename: str, filetype: str): pass filename = Path(filename) - # Performing check to see if the file exists or not - output_path = Path.cwd() / "reachability.json" - - if output_path.exists(): - with open(output_path, "r") as json_file: - database = json.load(json_file) - else: - database = {} + database = {} try: if not filename.exists(): @@ -60,12 +53,11 @@ def reachability(filename: str, filetype: str): for func in project.loader.main_object.symbols if func.is_export ] # _exports is only available for PE files - database[filename.name] = {} # go through every exported function for exp_addr in exports: exp_name = cfg.functions.get(exp_addr).name - database[filename.name][exp_name] = {} + database[exp_name] = {} # goes through every function that is reachable from exported function for imported_address in cfg.functions.callgraph.successors(exp_addr): @@ -75,16 +67,14 @@ def reachability(filename: str, filetype: str): if imported_function.name in project.loader.main_object.imports.keys(): library = lookup[imported_function.name] - if library not in database[filename.name][exp_name].keys(): - database[filename.name][exp_name][library] = [] + if library not in database[exp_name].keys(): + database[exp_name][library] = [] # adds our reachable imported function as a dependency - if imported_function.name not in database[filename.name][exp_name][library]: - database[filename.name][exp_name][library].append(imported_function.name) + if imported_function.name not in database[exp_name][library]: + database[exp_name][library].append(imported_function.name) - # Write the string_dict to the output JSON file - with open(output_path, "w") as json_file: - json.dump(database, json_file, indent=4) + return database except CLECompatibilityError as e: logger.info(f"Angr Error {filename} {e}") \ No newline at end of file From e96a8cbeaf8193268fc7204dd7a1b2132312ace8 Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Thu, 12 Jun 2025 17:33:55 -0700 Subject: [PATCH 13/18] Final fix for reachability 1.0 --- plugins/reachability/surfactantplugin_reachability.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index dca70d115..6aac1cecf 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -77,3 +77,4 @@ def reachability(filename: str, filetype: str): except CLECompatibilityError as e: logger.info(f"Angr Error {filename} {e}") + return None From 70e68af20f8113e3863d547e47f80cf7a98562e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:34:25 +0000 Subject: [PATCH 14/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/reachability/surfactantplugin_reachability.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index 6aac1cecf..ffe8e87d0 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -2,7 +2,6 @@ # See the top-level LICENSE file for details. # # SPDX-License-Identifier: MIT -import json from pathlib import Path import angr From d634754ed4037ff4d68c1b7127ec808f044743a1 Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Mon, 23 Jun 2025 14:45:27 -0700 Subject: [PATCH 15/18] added to the metadata section in the json output --- plugins/reachability/surfactantplugin_reachability.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index ffe8e87d0..f6cf8c7e2 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -9,12 +9,12 @@ from loguru import logger import surfactant.plugin - +from surfactant.sbomtypes import SBOM, Software @surfactant.plugin.hookimpl(specname="extract_file_info") # extract_strings(sbom: SBOM, software: Software, filename: str, filetype: str): # def angrimport_finder(filename: str, filetype: str, filehash: str): -def reachability(filename: str, filetype: str): +def reachability(sbom: SBOM, software: Software, filename: str, filetype: str): """ :param sbom(SBOM): The SBOM that the software entry/file is being added to. Can be used to add observations or analysis data. :param software(Software): The software entry associated with the file to extract information from. @@ -27,6 +27,9 @@ def reachability(filename: str, filetype: str): pass filename = Path(filename) + print(sbom) + print(software) + database = {} try: @@ -72,7 +75,7 @@ def reachability(filename: str, filetype: str): if imported_function.name not in database[exp_name][library]: database[exp_name][library].append(imported_function.name) - return database + return {"export_fn_reachability": database} except CLECompatibilityError as e: logger.info(f"Angr Error {filename} {e}") From 8102e108ed8c2a9cbab5d87a1f5ff4c98c0710f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 21:45:54 +0000 Subject: [PATCH 16/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/reachability/surfactantplugin_reachability.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index f6cf8c7e2..7c5fb728b 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -11,6 +11,7 @@ import surfactant.plugin from surfactant.sbomtypes import SBOM, Software + @surfactant.plugin.hookimpl(specname="extract_file_info") # extract_strings(sbom: SBOM, software: Software, filename: str, filetype: str): # def angrimport_finder(filename: str, filetype: str, filehash: str): From 9dc83fc66468fcb314decd21588f2b223a28b616 Mon Sep 17 00:00:00 2001 From: Seth Bredbenner Date: Mon, 7 Jul 2025 14:43:26 -0700 Subject: [PATCH 17/18] added error checking to reachability plugin --- .../surfactantplugin_reachability.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index f6cf8c7e2..1db66b336 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -5,6 +5,7 @@ from pathlib import Path import angr +import pyvex from cle import CLECompatibilityError from loguru import logger @@ -27,9 +28,6 @@ def reachability(sbom: SBOM, software: Software, filename: str, filetype: str): pass filename = Path(filename) - print(sbom) - print(software) - database = {} try: @@ -42,23 +40,28 @@ def reachability(sbom: SBOM, software: Software, filename: str, filetype: str): # library dependencies {import: library} lookup = {} for obj in project.loader.main_object.imports.keys(): - library = project.loader.find_symbol(obj).owner.provides - lookup[obj] = library + library = project.loader.find_symbol(obj) + if library is None: + continue + lookup[obj] = library.owner.provides # recreates our angr project without the libraries loaded to save on time project = angr.Project(filename, load_options={"auto_load_libs": False}) - cfg = project.analyses.CFGFast() - # holds every export address error is here exports = [ func.rebased_addr for func in project.loader.main_object.symbols if func.is_export ] # _exports is only available for PE files + cfg = project.analyses.CFGFast(start_at_entry=False, force_smart_scan=False, function_starts=exports) + # go through every exported function for exp_addr in exports: - exp_name = cfg.functions.get(exp_addr).name - database[exp_name] = {} + exp_name = cfg.functions.get(exp_addr) + if exp_name is None: + continue + database[exp_name.name] = {} + exp_name = exp_name.name # goes through every function that is reachable from exported function for imported_address in cfg.functions.callgraph.successors(exp_addr): From 622a38dea7c61086a9f486ebec842992968c02dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:53:55 +0000 Subject: [PATCH 18/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/reachability/surfactantplugin_reachability.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/reachability/surfactantplugin_reachability.py b/plugins/reachability/surfactantplugin_reachability.py index 226fa6e7b..03f37a0f5 100644 --- a/plugins/reachability/surfactantplugin_reachability.py +++ b/plugins/reachability/surfactantplugin_reachability.py @@ -5,7 +5,6 @@ from pathlib import Path import angr -import pyvex from cle import CLECompatibilityError from loguru import logger @@ -54,14 +53,16 @@ def reachability(sbom: SBOM, software: Software, filename: str, filetype: str): func.rebased_addr for func in project.loader.main_object.symbols if func.is_export ] # _exports is only available for PE files - cfg = project.analyses.CFGFast(start_at_entry=False, force_smart_scan=False, function_starts=exports) + cfg = project.analyses.CFGFast( + start_at_entry=False, force_smart_scan=False, function_starts=exports + ) # go through every exported function for exp_addr in exports: exp_name = cfg.functions.get(exp_addr) if exp_name is None: continue - database[exp_name.name] = {} + database[exp_name.name] = {} exp_name = exp_name.name # goes through every function that is reachable from exported function