source/interprocedural_analyses/taint/taintReporting.ml (198 lines of code) (raw):
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open Core
open Pyre
open Ast
open Interprocedural
module Json = Yojson.Safe
let get_result callable =
FixpointState.get_result callable |> AnalysisResult.get_result TaintResult.kind
let get_model callable =
FixpointState.get_model callable >>= AnalysisResult.get_model TaintResult.kind
let get_errors result = List.map ~f:Issue.to_error result
let issues_to_json ~filename_lookup result_opt =
match result_opt with
| None -> []
| Some issues ->
let issue_to_json issue =
let json = Issue.to_json ~filename_lookup issue in
`Assoc ["kind", `String "issue"; "data", json]
in
List.map ~f:issue_to_json issues
let metadata () =
let codes = Issue.code_metadata () in
`Assoc ["codes", codes]
let statistics () =
let model_verification_errors =
ModelVerificationError.get () |> List.map ~f:ModelVerificationError.to_json
in
`Assoc ["model_verification_errors", `List model_verification_errors]
let extract_errors scheduler callables =
let extract_errors callables =
List.filter_map ~f:(fun callable -> get_result callable >>| get_errors) callables
|> List.concat_no_order
in
Scheduler.map_reduce
scheduler
~policy:(Scheduler.Policy.legacy_fixed_chunk_count ())
~initial:[]
~map:(fun _ callables -> extract_errors callables)
~reduce:List.cons
~inputs:callables
()
|> List.concat_no_order
let externalize ~filename_lookup callable result_option model =
let issues = issues_to_json ~filename_lookup result_option in
if not (Model.should_externalize model) then
issues
else
Model.to_json ~filename_lookup callable model :: issues
let fetch_and_externalize ~filename_lookup callable =
let model = get_model callable |> Option.value ~default:Model.empty_model in
let result_option = get_result callable in
externalize ~filename_lookup callable result_option model
let emit_externalization ~filename_lookup emitter callable =
fetch_and_externalize ~filename_lookup callable |> List.iter ~f:emitter
let save_results_to_directory
~result_directory
~local_root
~filename_lookup
~skipped_overrides
~callables
~errors
=
let emit_json_array_elements out_buffer =
let seen_element = ref false in
fun json ->
if !seen_element then (
Bi_outbuf.add_string out_buffer "\n";
Json.to_outbuf out_buffer json)
else (
seen_element := true;
Json.to_outbuf out_buffer json)
in
let timer = Timer.start () in
let models_path analysis_name = Format.sprintf "%s-output.json" analysis_name in
let root = local_root |> PyrePath.absolute in
let save_models () =
let filename = models_path TaintResult.name in
let output_path = PyrePath.append result_directory ~element:filename in
let out_channel = open_out (PyrePath.absolute output_path) in
let out_buffer = Bi_outbuf.create_channel_writer out_channel in
let array_emitter = emit_json_array_elements out_buffer in
let header_with_version =
`Assoc ["file_version", `Int 3; "config", `Assoc ["repo", `String root]]
in
Json.to_outbuf out_buffer header_with_version;
Bi_outbuf.add_string out_buffer "\n";
Target.Set.iter (emit_externalization ~filename_lookup array_emitter) callables;
Bi_outbuf.flush_output_writer out_buffer;
close_out out_channel
in
let save_errors () =
let filename = "errors.json" in
let output_path = PyrePath.append result_directory ~element:filename in
let out_channel = open_out (PyrePath.absolute output_path) in
let out_buffer = Bi_outbuf.create_channel_writer out_channel in
Json.to_outbuf out_buffer (`List errors);
Bi_outbuf.flush_output_writer out_buffer;
close_out out_channel
in
let save_metadata () =
let filename = Format.sprintf "%s-metadata.json" TaintResult.name in
let output_path = PyrePath.append result_directory ~element:filename in
let out_channel = open_out (PyrePath.absolute output_path) in
let out_buffer = Bi_outbuf.create_channel_writer out_channel in
let filename_spec = models_path TaintResult.name in
let statistics =
let global_statistics =
`Assoc
[
( "skipped_overrides",
`List
(List.map skipped_overrides ~f:(fun override -> `String (Reference.show override)))
);
]
in
Json.Util.combine global_statistics (statistics ())
in
let toplevel_metadata =
`Assoc
[
"filename_spec", `String filename_spec;
"root", `String root;
"tool", `String "pysa";
"version", `String (Version.version ());
"stats", statistics;
]
in
let analysis_metadata = metadata () in
Json.Util.combine toplevel_metadata analysis_metadata |> Json.to_outbuf out_buffer;
Bi_outbuf.flush_output_writer out_buffer;
close_out out_channel
in
save_models ();
save_metadata ();
save_errors ();
Log.info "Analysis results were written to `%s`." (PyrePath.absolute result_directory);
Statistics.performance
~name:"Wrote analysis results"
~phase_name:"Writing analysis results"
~timer
()
let report
~scheduler
~static_analysis_configuration:
{
Configuration.StaticAnalysis.result_json_path;
configuration = { local_root; show_error_traces; _ };
_;
}
~environment:_
~filename_lookup
~callables
~skipped_overrides
~fixpoint_timer
~fixpoint_iterations
=
let errors = extract_errors scheduler (Target.Set.elements callables) in
(* Log and record stats *)
Log.info "Found %d issues" (List.length errors);
(match fixpoint_iterations with
| Some iterations ->
Log.info "Fixpoint iterations: %d" iterations;
Statistics.performance
~name:"Analysis fixpoint complete"
~phase_name:"Static analysis fixpoint"
~timer:fixpoint_timer
~integers:
[
"pysa fixpoint iterations", iterations;
"pysa heap size", SharedMemory.heap_size ();
"pysa issues", List.length errors;
]
()
| None -> ());
(* Dump results to output directory if one was provided, and return a list of json (empty whenever
we dumped to a directory) to summarize *)
let error_to_json error =
error
|> Interprocedural.Error.instantiate ~show_error_traces ~lookup:filename_lookup
|> Interprocedural.Error.Instantiated.to_yojson
in
let errors = List.map errors ~f:error_to_json in
match result_json_path with
| Some result_directory ->
save_results_to_directory
~result_directory
~local_root
~filename_lookup
~skipped_overrides
~callables
~errors;
[]
| _ -> errors