(*
 * Copyright (c) 2016 - present Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *)

open! Utils

module L = Logging

open PatternMatch

(* list of sources that return a tainted value *)
let sources0 = [
  (* for testing only *)
  {
    classname = "com.facebook.infer.builtins.InferTaint";
    method_name = "inferSecretSource";
    ret_type = "java.lang.Object";
    params = [];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Java;
  };
  {
    classname = "com.facebook.infer.builtins.InferTaint";
    method_name = "inferSecretSourceUndefined";
    ret_type = "java.lang.Object";
    params = [];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Java
  };
  (* actual specs *)
  {
    classname = "android.content.SharedPreferences";
    method_name = "getString";
    ret_type = "java.lang.String";
    params = ["java.lang.String"; "java.lang.String"];
    is_static = false;
    taint_kind = Tk_shared_preferences_data;
    language = Config.Java
  };
  (* === iOS === *)
  {
    classname = "NSHTTPCookie";
    method_name = "value";
    ret_type = "NSString *";
    params = [];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Clang
  };

] @ FbTaint.sources

(* list of (sensitive sinks, zero-indexed numbers of parameters that should not be tainted). note:
   index 0 means "the first non-this/self argument"; we currently don't have a way to say "this/self
   should not be tainted" with this form of specification *)
let sinks = [
  (* for testing only *)
  ({
    classname = "com.facebook.infer.builtins.InferTaint";
    method_name = "inferSensitiveSink";
    ret_type = "void";
    params = ["java.lang.Object"];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Java
  }, [0]);
  ({
    classname = "com.facebook.infer.builtins.InferTaint";
    method_name = "inferSensitiveSinkUndefined";
    ret_type = "void";
    params = ["java.lang.Object"];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Java
  }, [0]);
  (* actual specs *)
  ({
    classname = "android.util.Log";
    method_name = "d";
    ret_type = "int";
    params = ["java.lang.String"; "java.lang.String"];
    is_static = true;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java
  }, [0;1]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openInputStream";
    ret_type = "java.io.InputStream";
    params = ["android.net.Uri"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [1]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openOutputStream";
    ret_type = "java.io.OutputStream";
    params = ["android.net.Uri"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openOutputStream";
    ret_type = "java.io.OutputStream";
    params = ["android.net.Uri"; "java.lang.String"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openAssetFileDescriptor";
    ret_type = "android.content.res.AssetFileDescriptor";
    params = ["android.net.Uri"; "java.lang.String"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openAssetFileDescriptor";
    ret_type = "android.content.res.AssetFileDescriptor";
    params = ["android.net.Uri"; "java.lang.String"; "android.os.CancellationSignal"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openFileDescriptor";
    ret_type = "android.os.ParcelFileDescriptor";
    params = ["android.net.Uri"; "java.lang.String"; "android.os.CancellationSignal"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openFileDescriptor";
    ret_type = "android.os.ParcelFileDescriptor";
    params = ["android.net.Uri"; "java.lang.String"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openTypedAssetFileDescriptor";
    ret_type = "android.content.res.AssetFileDescriptor";
    params = ["android.net.Uri"; "java.lang.String"; "android.os.Bundle";
              "android.os.CancellationSignal"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);
  ({
    classname = "android.content.ContentResolver";
    method_name = "openTypedAssetFileDescriptor";
    ret_type = "android.content.res.AssetFileDescriptor";
    params = ["android.net.Uri"; "java.lang.String"; "android.os.Bundle"];
    is_static = false;
    taint_kind = Tk_privacy_annotation;
    language = Config.Java;
  }, [0]);

  (* === iOS === *)
  ({
    classname = "NSString";
    method_name = "stringWithFormat:";
    ret_type = "instancetype";
    params = [];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Clang;
  }, [-2]);
  ({
    classname = "NSString";
    method_name = "stringWithUTF8String:";
    ret_type = "instancetype";
    params = [];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Clang
  }, [-2]);
  ({
    classname = "NSString";
    method_name = "localizedStringWithFormat:";
    ret_type = "instancetype";
    params = [];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Clang
  }, [-2]);
  ({
    classname = "NSString";
    method_name = "initWithFormat:";
    ret_type = "instancetype";
    params = [];
    is_static = false;
    taint_kind = Tk_unknown;
    language = Config.Clang
  }, [-2]);
  ({
    classname = "NSString";
    method_name = "stringWithString:";
    ret_type = "instancetype";
    params = [];
    is_static = true;
    taint_kind = Tk_unknown;
    language = Config.Clang
  }, [0]);

  (*  ==== iOS for testing only ==== *)
  ({
    classname = "ExampleViewController";
    method_name = "loadURL:trackingCodes:";
    ret_type = "void";
    params = [];
    is_static = false;
    taint_kind = Tk_unknown;
    language = Config.Clang;
  }, [1]); (* it's instance method *)
] @ FbTaint.sinks

let functions_with_tainted_params = [
  (*  ==== iOS for testing only ==== *)
  ({
    classname = "ExampleDelegate";
    method_name = "application:openURL:sourceApplication:annotation:";
    ret_type = "BOOL";
    params = [];
    is_static = false; (* it's instance method *)
    taint_kind = Tk_unknown;
    language = Config.Clang;
  }, [2]);

  (* actual specs *)
  ({ (* This method is a source in iOS as it get as parameter
        a non trusted URL (openURL). The method the passes
        it around and this URL may arrive unsanitized to
        loadURL:trackingCodes: of FBWebViewController
        which uses the URL. *)
    classname = "AppDelegate";
    method_name = "application:openURL:sourceApplication:annotation:";
    ret_type = "BOOL";
    params = [];
    is_static = false; (* it's instance method *)
    taint_kind = Tk_integrity_annotation;
    language = Config.Clang;
  }, [2]);
] @ FbTaint.functions_with_tainted_params

(* turn string specificiation of Java method into a procname *)
let java_method_to_procname java_method =
  Procname.Java
    (Procname.java
       (Procname.split_classname java_method.classname)
       (Some (Procname.split_classname java_method.ret_type))
       java_method.method_name
       (IList.map Procname.split_classname java_method.params)
       (if java_method.is_static then Procname.Static else Procname.Non_Static))

(* turn string specificiation of an objc method into a procname *)
let objc_method_to_procname objc_method =
  let method_kind = Procname.objc_method_kind_of_bool (not objc_method.is_static) in
  Procname.ObjC_Cpp
    (Procname.objc_cpp objc_method.classname objc_method.method_name method_kind)

let taint_spec_to_taint_info taint_spec =
  let taint_source =
    match taint_spec.language with
    | Config.Clang -> objc_method_to_procname taint_spec
    | Config.Java -> java_method_to_procname taint_spec in
  { PredSymb.taint_source; taint_kind = taint_spec.taint_kind }

let sources =
  IList.map taint_spec_to_taint_info sources0

let mk_pname_param_num methods =
  IList.map
    (fun (mname, param_num) -> taint_spec_to_taint_info mname, param_num)
    methods

let taint_sinks =
  mk_pname_param_num sinks

let func_with_tainted_params =
  mk_pname_param_num functions_with_tainted_params

let attrs_opt_get_annots = function
  | Some attrs -> attrs.ProcAttributes.method_annotation
  | None -> Annot.Method.empty

(* TODO: return a taint kind *)
(** returns true if [callee_pname] returns a tainted value *)
let returns_tainted callee_pname callee_attrs_opt =
  let procname_matches taint_info =
    Procname.equal taint_info.PredSymb.taint_source callee_pname in
  try
    let taint_info = IList.find procname_matches sources in
    Some taint_info.PredSymb.taint_kind
  with Not_found ->
    let ret_annot, _ = attrs_opt_get_annots callee_attrs_opt in
    if Annotations.ia_is_integrity_source ret_annot
    then Some PredSymb.Tk_integrity_annotation
    else if Annotations.ia_is_privacy_source ret_annot
    then Some PredSymb.Tk_privacy_annotation
    else None

let find_callee taint_infos callee_pname =
  try
    Some
      (IList.find
         (fun (taint_info, _) -> Procname.equal taint_info.PredSymb.taint_source callee_pname)
         taint_infos)
  with Not_found -> None

(** returns list of zero-indexed argument numbers of [callee_pname] that may be tainted *)
let accepts_sensitive_params callee_pname callee_attrs_opt =
  match find_callee taint_sinks callee_pname with
  | None ->
      let _, param_annots = attrs_opt_get_annots callee_attrs_opt in
      let offset = if Procname.java_is_static callee_pname then 0 else 1 in
      let indices_and_annots =
        IList.mapi (fun param_num attr  -> param_num + offset, attr) param_annots in
      let tag_tainted_indices acc (index, attr) =
        if Annotations.ia_is_integrity_sink attr
        then (index, PredSymb.Tk_privacy_annotation) :: acc
        else if Annotations.ia_is_privacy_sink attr
        then (index, PredSymb.Tk_privacy_annotation) :: acc
        else acc in
      IList.fold_left tag_tainted_indices [] indices_and_annots
  | Some (taint_info, tainted_param_indices) ->
      IList.map (fun param_num -> param_num, taint_info.PredSymb.taint_kind) tainted_param_indices

(** returns list of zero-indexed parameter numbers of [callee_pname] that should be
    considered tainted during symbolic execution *)
let tainted_params callee_pname =
  match find_callee func_with_tainted_params callee_pname with
  | Some (taint_info, tainted_param_indices) ->
      IList.map (fun param_num -> param_num, taint_info.PredSymb.taint_kind) tainted_param_indices
  | None -> []

let has_taint_annotation fieldname (struct_typ: StructTyp.t) =
  let fld_has_taint_annot (fname, _, annot) =
    Ident.fieldname_equal fieldname fname &&
    (Annotations.ia_is_privacy_source annot || Annotations.ia_is_integrity_source annot) in
  IList.exists fld_has_taint_annot struct_typ.fields ||
  IList.exists fld_has_taint_annot struct_typ.statics

(* add tainting attributes to a list of paramenters *)
let get_params_to_taint tainted_param_nums formal_params =
  let get_taint_kind index =
    try Some (IList.find (fun (taint_index, _) -> index = taint_index) tainted_param_nums)
    with Not_found -> None in
  let collect_params_to_taint params_to_taint_acc (index, param) =
    match get_taint_kind index with
    | Some (_, taint_kind) -> (param, taint_kind) :: params_to_taint_acc
    | None -> params_to_taint_acc in
  let numbered_params = IList.mapi (fun i param -> (i, param)) formal_params in
  IList.fold_left collect_params_to_taint [] numbered_params

(* add tainting attribute to a pvar in a prop *)
let add_tainting_attribute tenv att pvar_param prop =
  IList.fold_left
    (fun prop_acc hpred ->
       match hpred with
       | Sil.Hpointsto (Exp.Lvar pvar, (Sil.Eexp (rhs, _)), _)
         when Pvar.equal pvar pvar_param ->
           L.d_strln ("TAINT ANALYSIS: setting taint/untaint attribute of parameter " ^
                      (Pvar.to_string pvar));
           Attribute.add_or_replace tenv prop_acc (Apred (att, [rhs]))
       | _ -> prop_acc)
    prop prop.Prop.sigma
