lib/scanner_module.ex

# Copyright (C) 2020 by the Georgia Tech Research Institute (GTRI)
# This software may be modified and distributed under the terms of
# the BSD 3-Clause license. See the LICENSE file for details.

defmodule ScannerModule do
  @moduledoc """
  Scanner scans.
  """

  def dependencies(path) do
    cwd = File.cwd!()

    File.cd!(path)

    deps =
      File.read!("./mix.lock")
      |> Hex.Lockfile.parse!(true)
      |> Hex.Encoder.lockfile_json()

    File.cd!(cwd)
    deps
  end

  def scan(path) do
    cwd = File.cwd!()

    File.cd!(path)
    start_time = DateTime.utc_now()

    project_types_identified = ProjectIdent.get_project_types_identified(path)

    mix? = Map.has_key?(project_types_identified, :mix)
    node? = Map.has_key?(project_types_identified, :node)
    pypi? = Map.has_key?(project_types_identified, :python)

    {hex_reports_list, hex_deps_count} = Hex.Scanner.scan(mix?, project_types_identified)

    {json_reports_list, yarn_reports_list, npm_deps_count} =
      Npm.Scanner.scan(node?, project_types_identified)

    {pypi_reports_list, pypi_deps_count} = Pypi.Scanner.scan(pypi?, project_types_identified)

    reports = [
      hex: hex_reports_list,
      node_json: json_reports_list,
      node_yarn: yarn_reports_list,
      pypi: pypi_reports_list
    ]

    result =
      get_report(
        start_time,
        hex_deps_count + npm_deps_count + pypi_deps_count,
        reports,
        project_types_identified
      )

    File.cd!(cwd)

    Poison.encode!(result, pretty: true)
  end

  def get_report(
        start_time,
        deps_count,
        [hex: hex_report, node_json: json_report, node_yarn: yarn_report, pypi: pypi_report] =
          reports,
        project_types
      ) do
    files =
      Enum.map(reports, fn {k, v} -> if !Enum.empty?(v), do: k end)
      |> Enum.reject(fn k -> k == nil end)

    cond do
      # If both package-lock.json and yarn.lock are present, create 2 separate reports
      Enum.member?(files, :node_json) && Enum.member?(files, :node_yarn) ->
        # json
        result_json_list = hex_report ++ json_report
        result_yarn_list = hex_report ++ yarn_report

        result_json = %{
          :state => :complete,
          :metadata => %{
            repo_count: Enum.count(result_json_list),
            dependency_count: deps_count
          },
          :report => %{
            :uuid => UUID.uuid1(),
            :repos => result_json_list
          }
        }

        result_yarn = %{
          :state => :complete,
          :metadata => %{
            repo_count: Enum.count(result_yarn_list),
            dependency_count: deps_count
          },
          :report => %{
            :uuid => UUID.uuid1(),
            :repos => result_yarn_list
          }
        }

        result_json = AnalyzerModule.determine_risk_counts(result_json)
        result_yarn = AnalyzerModule.determine_risk_counts(result_yarn)

        end_time = DateTime.utc_now()

        result = %{
          :scan_node_json => result_json,
          :scan_node_yarn => result_yarn,
          :metadata => %{
            :times => %{
              start_time: DateTime.to_iso8601(start_time),
              end_time: DateTime.to_iso8601(end_time),
              duration: DateTime.diff(end_time, start_time)
            },
            :files => project_types
          }
        }

        result |> Map.put(:scan_node_yarn, result_yarn)

      Enum.empty?(project_types) ->
        %{:error => "No dependency manifest files were found"}

      true ->
        result_list = hex_report ++ json_report ++ yarn_report ++ pypi_report

        result = %{
          :state => :complete,
          :metadata => %{
            repo_count: Enum.count(result_list),
            dependency_count: deps_count
          },
          :report => %{
            :uuid => UUID.uuid1(),
            :repos => result_list
          },
          :files => files
        }

        result = AnalyzerModule.determine_risk_counts(result)

        end_time = DateTime.utc_now()

        times = %{
          start_time: DateTime.to_iso8601(start_time),
          end_time: DateTime.to_iso8601(end_time),
          duration: DateTime.diff(end_time, start_time)
        }

        metadata = Map.put_new(result[:metadata], :times, times)
        result |> Map.put(:metadata, metadata)
    end
  end
end