Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyser: Add new standalone analyser for all-public-candidates #2014

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/fuzz_introspector/analyses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from fuzz_introspector.analyses import annotated_cfg
from fuzz_introspector.analyses import source_code_line_analyser
from fuzz_introspector.analyses import far_reach_low_coverage_analyser
from fuzz_introspector.analyses import public_candidate_analyser

# All optional analyses.
# Ordering here is important as top analysis will be shown first in the report
Expand All @@ -47,4 +48,5 @@
standalone_analyses: list[type[analysis.AnalysisInterface]] = [
source_code_line_analyser.SourceCodeLineAnalyser,
far_reach_low_coverage_analyser.FarReachLowCoverageAnalyser,
public_candidate_analyser.PublicCandidateAnalyser,
]
153 changes: 153 additions & 0 deletions src/fuzz_introspector/analyses/public_candidate_analyser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright 2025 Fuzz Introspector Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Analysis plugin for introspection to extract all publicly accessible
non-standard library functions."""

import os
import json
import logging

from typing import (Any, List, Dict)

from fuzz_introspector import (analysis, html_helpers)

from fuzz_introspector.datatypes import (project_profile, fuzzer_profile,
function_profile)

logger = logging.getLogger(name=__name__)


class PublicCandidateAnalyser(analysis.AnalysisInterface):
"""Exract all public non-standard libary functions fron the project."""

name: str = 'PublicCandidateAnalyser'

def __init__(self) -> None:
self.json_results: Dict[str, Any] = {}
self.json_string_result = ''

@classmethod
def get_name(cls):
"""Return the analyser identifying name for processing.

:return: The identifying name of this analyser
:rtype: str
"""
return cls.name

def get_json_string_result(self) -> str:
"""Return the stored json string result.

:return: The json string result processed and stored
by this analyser
:rtype: str
"""
if self.json_string_result:
return self.json_string_result
return json.dumps(self.json_results)

def set_json_string_result(self, string):
"""Store the result of this analyser as json string result
for further processing in a later time.

:param json_string: A json string variable storing the
processing result of the analyser for future use
:type json_string: str
"""
self.json_string_result = string

def analysis_func(self,
table_of_contents: html_helpers.HtmlTableOfContents,
tables: List[str],
proj_profile: project_profile.MergedProjectProfile,
profiles: List[fuzzer_profile.FuzzerProfile],
basefolder: str, coverage_url: str,
conclusions: List[html_helpers.HTMLConclusion],
out_dir: str) -> str:
logger.info(' - Running analysis %s', self.get_name())

# Get all functions from the profiles
all_functions = list(proj_profile.all_functions.values())
all_functions.extend(proj_profile.all_constructors.values())

# Filter and sort functions
filtered_functions = self._filter_functions(all_functions)
sorted_functions = self._sort_functions(filtered_functions,
proj_profile)

# Convert functions to dict
result_list = [
function.to_dict(
proj_profile.get_func_hit_percentage(function.function_name))
for function in sorted_functions
]

result_json_path = os.path.join(out_dir, 'result.json')
logger.info('Found %d function candidiates.', len(result_list))
logger.info('Dumping result to %s', result_json_path)
with open(result_json_path, 'w') as f:
json.dump(result_list, f)

return ''

def _filter_functions(
self, functions: list[function_profile.FunctionProfile]
) -> list[function_profile.FunctionProfile]:
"""Filter unrelated functions in a provided function list if
it meet any of the following conditions.
1) Fuzzing related methods / functions
2) Functions with name contains word "exception / error / test"
"""
excluded_function_name = [
'fuzzertestoneinput', 'fuzzerinitialize', 'fuzzerteardown',
'exception', 'error', 'test', 'llvmfuzertestoneinput',
'fuzz_target'
]

return [
function for function in functions
if (function.is_accessible and not function.is_jvm_library
and function.arg_count > 0 and not any(
function_name in function.function_name.lower()
for function_name in excluded_function_name))
]

def _sort_functions(
self,
functions: list[function_profile.FunctionProfile],
proj_profile: project_profile.MergedProjectProfile,
) -> list[function_profile.FunctionProfile]:
"""Sort the function list according to the following criteria in order.
The order is acscending unless otherwise specified.
For boolean sorting, False is always come first in acscending order.
1) If the function is reached by any existing fuzzers.
2) If the function belongs to a enum class (only for JVM project).
3) The runtime code coverage of the function.
4) The function call depth in descending order.
5) The cyclomatic complexity of the function in descending order.
6) The undiscovered complexity of the function.
7) The number of arguments of this function in descending order.
8) Number of source code lines in descending order.
9) The number of how many fuzzers reached this target function.
"""
return sorted(
functions,
key=lambda item:
(bool(item.reached_by_fuzzers), item.is_enum,
proj_profile.get_func_hit_percentage(item.function_name), -item.
function_depth, -item.cyclomatic_complexity, item.
new_unreached_complexity, -item.arg_count, -(
item.function_line_number_end - item.function_linenumber),
len(item.reached_by_fuzzers)),
reverse=False)
24 changes: 23 additions & 1 deletion src/fuzz_introspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ def get_cmdline_parser() -> argparse.ArgumentParser:
required=True,
help="""
Available analyser:
SourceCodeLineAnalyser FarReachLowCoverageAnalyser""")
SourceCodeLineAnalyser FarReachLowCoverageAnalyser
PublicCandidateAnalyser""")

source_code_line_analyser_parser = analyser_parser.add_parser(
'SourceCodeLineAnalyser',
Expand Down Expand Up @@ -218,6 +219,27 @@ def get_cmdline_parser() -> argparse.ArgumentParser:
type=str,
help='Folder to store analysis results.')

public_candidate_analyser_parser = analyser_parser.add_parser(
'PublicCandidateAnalyser',
help=('Provide publicly accessible non-standard library functions '
'for the project that are good targets for fuzzing.'))

public_candidate_analyser_parser.add_argument(
'--target-dir',
type=str,
help='Directory holding source to analyse.',
required=True)
public_candidate_analyser_parser.add_argument(
'--language',
type=str,
help='Programming of the source code to analyse.',
choices=constants.LANGUAGES_SUPPORTED)
public_candidate_analyser_parser.add_argument(
'--out-dir',
default='',
type=str,
help='Folder to store analysis results.')

return parser


Expand Down
17 changes: 6 additions & 11 deletions src/fuzz_introspector/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,12 @@ def analyse(args) -> int:
introspection_proj = analysis.IntrospectionProject(language, out_dir, '')
introspection_proj.load_data_files(True, '', out_dir)

# Perform the chosen standalone analysis
# Perform specific actions for certain standalone analyser
if target_analyser.get_name() == 'SourceCodeLineAnalyser':
source_file = args.source_file
source_line = args.source_line

target_analyser.set_source_file_line(source_file, source_line)
target_analyser.analysis_func(html_helpers.HtmlTableOfContents(), [],
introspection_proj.proj_profile,
introspection_proj.profiles, '', '', [],
out_dir)
elif target_analyser.get_name() == 'FarReachLowCoverageAnalyser':
exclude_static_functions = args.exclude_static_functions
only_referenced_functions = args.only_referenced_functions
Expand All @@ -229,11 +225,10 @@ def analyse(args) -> int:
target_analyser.set_max_functions(max_functions)
target_analyser.set_introspection_project(introspection_proj)

target_analyser.analysis_func(html_helpers.HtmlTableOfContents(), [],
introspection_proj.proj_profile,
introspection_proj.profiles, '', '', [],
out_dir)

# TODO Add more analyser for standalone run
# Run the analyser
target_analyser.analysis_func(html_helpers.HtmlTableOfContents(), [],
introspection_proj.proj_profile,
introspection_proj.profiles, '', '', [],
out_dir)

return constants.APP_EXIT_SUCCESS
Loading