# 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 Npm.Scanner do
require HTTPoison.Retry
@moduledoc """
Scanner scans for node dependencies to run analysis on.
"""
@doc """
scan: called when node? is false, returning an empty list and 0
"""
def scan(node?, _project_types) when node? == false, do: {[], [], 0}
@doc """
scan: takes in a path to node dependencies and returns the
dependencies mapped to their analysis and the number of dependencies
"""
def scan(_node?, %{node: paths_to_npm_files}, option \\ ".") do
path_to_package_json =
Enum.find(paths_to_npm_files, &String.ends_with?(&1, "package#{option}json"))
path_to_package_lock =
Enum.find(paths_to_npm_files, &String.ends_with?(&1, "lock#{option}json"))
path_to_yarn_lock = Enum.find(paths_to_npm_files, &String.contains?(&1, "yarn#{option}lock"))
if path_to_package_json do
{:ok, {direct_deps, deps_count}} =
File.read!(path_to_package_json)
|> Npm.Packagefile.parse!()
cond do
path_to_package_lock && path_to_yarn_lock ->
{:ok, {json_lib_map, _count}} =
File.read!(path_to_package_lock)
|> Npm.Packagefile.parse!()
json_result_map =
Enum.map(json_lib_map, fn {lib, _version} ->
query_npm(lib)
end)
{:ok, {yarn_lib_map, _count}} =
File.read!(path_to_yarn_lock)
|> Npm.Yarnlockfile.parse!()
yarn_result_map =
Enum.map(yarn_lib_map, fn {lib, _version} ->
query_npm(lib)
end)
{json_result_map, yarn_result_map, deps_count}
path_to_package_lock ->
{:ok, {lib_map, _count}} =
File.read!(path_to_package_lock)
|> Npm.Packagefile.parse!()
result_map =
Enum.map(lib_map, fn {lib, _version} ->
query_npm(lib)
end)
{result_map, [], deps_count}
path_to_yarn_lock ->
{:ok, {lib_map, _count}} =
File.read!(path_to_yarn_lock)
|> Npm.Yarnlockfile.parse!()
result_map =
Enum.map(lib_map, fn {lib, _version} ->
query_npm(lib)
end)
{[], result_map, deps_count}
true ->
result_map =
Enum.map(direct_deps, fn {lib, _version} ->
query_npm(lib)
end)
{result_map, [], deps_count}
end
else
{:error, "Must contain a package.json file"}
end
end
@doc """
query_npm: function that takes in a package and returns an analysis
on that package's repository using analyser_module. If the package url cannot
be reached, an error is returned.
"""
def query_npm(package) do
encoded_id = URI.encode(package)
HTTPoison.start()
{:ok, response} =
HTTPoison.get("https://replicate.npmjs.com/" <> encoded_id)
|> HTTPoison.Retry.autoretry(
max_attempts: 5,
wait: 15000,
include_404s: false,
retry_unknown_errors: false
)
case response.status_code do
200 ->
repo_info = get_npm_repository(response.body)
if is_map(repo_info) && Map.has_key?(repo_info, "url") do
{:ok, report} = AnalyzerModule.analyze(repo_info["url"], "mix.scan", %{types: true})
report
else
{:ok, report} = AnalyzerModule.analyze(package, "mix.scan", %{types: true})
report
end
_ ->
{:ok, report} = AnalyzerModule.analyze(package, "mix.scan", %{types: true})
report
end
end
defp get_npm_repository(body) do
decoded = Poison.decode!(body)
repos =
case Map.has_key?(decoded, "repository") do
true -> decoded["repository"]
false -> %{error: "no repository"}
end
repos
end
end