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

analysis: Change analysis interface to allow passing in properties #1993

Merged
merged 16 commits into from
Jan 20, 2025
Merged
25 changes: 24 additions & 1 deletion src/fuzz_introspector/analyses/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 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.
"""Initialisation of AnalysisInterface instances"""

from fuzz_introspector import analysis
from fuzz_introspector.analyses import bug_digestor
from fuzz_introspector.analyses import driver_synthesizer
from fuzz_introspector.analyses import engine_input
Expand All @@ -8,10 +24,11 @@
from fuzz_introspector.analyses import runtime_coverage_analysis
from fuzz_introspector.analyses import sinks_analyser
from fuzz_introspector.analyses import annotated_cfg
from fuzz_introspector.analyses import source_code_line_analyser

# All optional analyses.
# Ordering here is important as top analysis will be shown first in the report
all_analyses = [
all_analyses: list[type[analysis.AnalysisInterface]] = [
optimal_targets.OptimalTargets,
engine_input.EngineInput,
runtime_coverage_analysis.RuntimeCoverageAnalysis,
Expand All @@ -23,3 +40,9 @@
sinks_analyser.SinkCoverageAnalyser,
annotated_cfg.FuzzAnnotatedCFG,
]

# This is the list of analyses that are meant to run
# directly from CLI without the need to generate HTML reports
standalone_analyses: list[type[analysis.AnalysisInterface]] = [
source_code_line_analyser.SourceCodeLineAnalyser,
]
135 changes: 135 additions & 0 deletions src/fuzz_introspector/analyses/source_code_line_analyser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# 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 of the function on target line in
target source file."""

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 SourceCodeLineAnalyser(analysis.AnalysisInterface):
"""Locate for the function in given line of given source file."""

name: str = 'SourceCodeLineAnalyser'

def __init__(self):
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 set_source_file_line(self, source_file: str, source_line: int):
"""Configure the source file and source line for this analyser."""
self.source_file = source_file
self.source_line = source_line

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())

if not self.source_file or self.source_line <= 0:
logger.error('No valid source code or target line are provided')
return ''

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

# Generate SourceFile to Function Profile map and store in JSON Result
func_file_map: dict[str, list[function_profile.FunctionProfile]] = {}
for function in all_functions:
func_list = func_file_map.get(function.function_source_file, [])
func_list.append(function)
func_file_map[function.function_source_file] = func_list

if os.sep in self.source_file:
# File path
target_func_list = func_file_map.get(self.source_file, [])
else:
# File name
target_func_list = []
for key, value in func_file_map.items():
if os.path.basename(key) == self.source_file:
target_func_list.extend(value)

if not target_func_list:
logger.error(
'Failed to locate the target source file %s from the project.',
self.source_file)

result_list = []
for func in target_func_list:
start = func.function_linenumber
end = func.function_line_number_end
if start <= self.source_line <= end:
logger.info('Found function %s from line %d in %s',
func.function_name, self.source_line,
self.source_file)
result_list.append(func.to_dict())

if result_list:
self.json_results['functions'] = result_list
result_json_path = os.path.join(out_dir, 'functions.json')
logger.info('Dumping result to %s', result_json_path)
with open(result_json_path, 'w') as f:
json.dump(self.json_results, f)
else:
logger.info('No functions found from line %d in %s',
self.source_line, self.source_file)

return ''
17 changes: 11 additions & 6 deletions src/fuzz_introspector/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@
import os
import shutil

from typing import (
Dict,
List,
Type,
Set,
)
from typing import (Dict, List, Type, Set, Union)

from fuzz_introspector import (cfg_load, code_coverage, constants, data_loader,
debug_info, html_helpers, json_report, utils)
Expand Down Expand Up @@ -173,6 +168,11 @@ class AnalysisInterface(abc.ABC):
json_string_result: str = ""
display_html: bool = False

def set_additional_properties(self, properties: dict[str, Union[str,
int]]):
"""Allow setting additional properties for this analysis."""
self.properties = properties

@abc.abstractmethod
def analysis_func(self,
table_of_contents: html_helpers.HtmlTableOfContents,
Expand Down Expand Up @@ -261,6 +261,11 @@ def get_all_analyses() -> List[Type[AnalysisInterface]]:
return analyses.all_analyses


def get_all_standalone_analyses() -> List[Type[AnalysisInterface]]:
from fuzz_introspector import analyses
return analyses.standalone_analyses


def callstack_get_parent(n: cfg_load.CalltreeCallsite, c: Dict[int,
str]) -> str:
return c[int(n.depth) - 1]
Expand Down
48 changes: 47 additions & 1 deletion src/fuzz_introspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ def get_cmdline_parser() -> argparse.ArgumentParser:
],
help="""
Analyses to run. Available options:
OptimalTargets, FuzzEngineInput, ThirdPartyAPICoverageAnalyser
AnnotatedCFG, BugDigestorAnalysis, FuzzCalltreeAnalysis,
FuzzDriverSynthesizerAnalysis, FuzzEngineInputAnalysis,
FilePathAnalyser, ThirdPartyAPICoverageAnalyser,
MetadataAnalysis, OptimalTargets, RuntimeCoverageAnalysis,
SinkCoverageAnalyser
""")
report_parser.add_argument("--enable-all-analyses",
action='store_true',
Expand Down Expand Up @@ -134,6 +138,46 @@ def get_cmdline_parser() -> argparse.ArgumentParser:
required=True,
help='Path to the second report')

# Standalone analyser
analyse_parser = subparsers.add_parser(
'analyse',
help='Standlone analyser commands to run on the target project.')

analyser_parser = analyse_parser.add_subparsers(
dest='analyser',
required=True,
help='Available analyser: SourceCodeLineAnalyser')

source_code_line_analyser_parser = analyser_parser.add_parser(
'SourceCodeLineAnalyser',
help=('Provide information in out-dir/function.json for the function'
' found in the given target file and line number'))
source_code_line_analyser_parser.add_argument(
'--source-file',
default='',
type=str,
help='Target file path or name for SourceCodeLineAnalyser')
source_code_line_analyser_parser.add_argument(
'--source-line',
default=-1,
type=int,
help='Target line for SourceCodeLineAnalyser')
source_code_line_analyser_parser.add_argument(
'--target-dir',
type=str,
help='Directory holding source to analyse.',
required=True)
source_code_line_analyser_parser.add_argument(
'--language',
type=str,
help='Programming of the source code to analyse.',
choices=constants.LANGUAGES_SUPPORTED)
source_code_line_analyser_parser.add_argument(
'--out-dir',
default='',
type=str,
help='Folder to store analysis results.')

return parser


Expand Down Expand Up @@ -176,6 +220,8 @@ def main() -> int:
return_code = commands.light_analysis(args)
elif args.command == 'full':
return_code = commands.end_to_end(args)
elif args.command == 'analyse':
return_code = commands.analyse(args)
else:
return_code = constants.APP_EXIT_ERROR
logger.info("Ending fuzz introspector post-processing")
Expand Down
71 changes: 68 additions & 3 deletions src/fuzz_introspector/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
import json
import yaml
import shutil
from typing import List, Optional
from typing import Optional

from fuzz_introspector import analysis
from fuzz_introspector import constants
from fuzz_introspector import diff_report
from fuzz_introspector import html_helpers
from fuzz_introspector import html_report
from fuzz_introspector import utils

Expand Down Expand Up @@ -54,6 +55,9 @@ def end_to_end(args) -> int:
else:
out_dir = os.getcwd()

if not os.path.exists(out_dir):
os.mkdir(out_dir)

if args.language == constants.LANGUAGES.JAVA:
entrypoint = 'fuzzerTestOneInput'
else:
Expand Down Expand Up @@ -81,12 +85,12 @@ def end_to_end(args) -> int:

def run_analysis_on_dir(target_folder: str,
coverage_url: str,
analyses_to_run: List[str],
analyses_to_run: list[str],
correlation_file: str,
enable_all_analyses: bool,
report_name: str,
language: str,
output_json: Optional[List[str]] = None,
output_json: Optional[list[str]] = None,
parallelise: bool = True,
dump_files: bool = True,
out_dir: str = '') -> int:
Expand Down Expand Up @@ -150,3 +154,64 @@ def light_analysis(args) -> int:
f.write(json.dumps(list(all_source_files)))

return 0


def analyse(args) -> int:
"""Perform a light analysis using the chosen Analyser and return
json results."""
# Retrieve the correct analyser
target_analyser = None
for analyser in analysis.get_all_standalone_analyses():
if analyser.get_name() == args.analyser:
target_analyser = analysis.instantiate_analysis_interface(analyser)
break

# Return error if analyser not found
if not target_analyser:
logger.error('Analyser %s not found.', args.analyser)
return constants.APP_EXIT_ERROR

# Auto detect project language is not provided
if not args.language:
args.language = utils.detect_language(args.target_dir)

# Prepare out directory
if args.out_dir:
out_dir = args.out_dir
else:
out_dir = os.getcwd()

if not os.path.exists(out_dir):
os.mkdir(out_dir)

# Fix entrypoint default for languages
if args.language == constants.LANGUAGES.JAVA:
entrypoint = 'fuzzerTestOneInput'
else:
entrypoint = 'LLVMFuzzerTestOneInput'

# Run the frontend
oss_fuzz.analyse_folder(language=args.language,
directory=args.target_dir,
entrypoint=entrypoint,
out=out_dir)

# Perform the FI backend project analysis from the frontend
introspection_proj = analysis.IntrospectionProject(args.language, out_dir,
'')
introspection_proj.load_data_files(True, '', out_dir)

# Perform the chosen standalone analysis
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)

# TODO Add more analyser for standalone run

return constants.APP_EXIT_SUCCESS
Loading
Loading