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

Add elixir_phoenix detector/analyzer (#161) #166

Merged
merged 3 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
| Python | FastAPI | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ruby | Rails | ✅ | ✅ | ✅ | ✅ | X |
| Ruby | Sinatra | ✅ | ✅ | ✅ | ✅ | X |
| Ruby | Hanami | ✅ | ✅ | | | X |
| Ruby | Hanami | ✅ | ✅ | X | X | X |
| Php | | ✅ | ✅ | ✅ | ✅ | X |
| Java | Jsp | ✅ | ✅ | ✅ | X | X |
| Java | Armeria | ✅ | ✅ | X | X | X |
| Java | Spring | ✅ | ✅ | X | X | X |
| Kotlin | Spring | ✅ | ✅ | X | X | X |
| JS | Express | ✅ | ✅ | X | X | X |
| Rust | Axum | ✅ | ✅ | X | X | X |
| Elixir | Phoenix | ✅ | ✅ | X | X | ✅ |
| C# | ASP.NET MVC | ✅ | X | X | X | X |
| JS | Next | X | X | X | X | X |

Expand Down
64 changes: 64 additions & 0 deletions spec/functional_test/fixtures/elixir_phoenix/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
import Config

config :elixir_phoenix,
ecto_repos: [ElixirPhoenix.Repo]

# Configures the endpoint
config :elixir_phoenix, ElixirPhoenixWeb.Endpoint,
url: [host: "localhost"],
render_errors: [
formats: [html: ElixirPhoenixWeb.ErrorHTML, json: ElixirPhoenixWeb.ErrorJSON],
layout: false
],
pubsub_server: ElixirPhoenix.PubSub,
live_view: [signing_salt: "WpjzJI0z"]

# Configures the mailer
#
# By default it uses the "Local" adapter which stores the emails
# locally. You can see the emails in your browser, at "/dev/mailbox".
#
# For production it's recommended to configure a different adapter
# at the `config/runtime.exs`.
config :elixir_phoenix, ElixirPhoenix.Mailer, adapter: Swoosh.Adapters.Local

# Configure esbuild (the version is required)
config :esbuild,
version: "0.14.41",
default: [
args:
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]

# Configure tailwind (the version is required)
config :tailwind,
version: "3.2.4",
default: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]

# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule ElixirPhoenixWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :elixir_phoenix

# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_elixir_phoenix_key",
signing_salt: "H7VlC99P",
same_site: "Lax"
]

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :elixir_phoenix,
gzip: false,
only: ElixirPhoenixWeb.static_paths()

# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :elixir_phoenix
end

plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"

plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()

plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug ElixirPhoenixWeb.Router
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule ElixirPhoenixWeb.Router do
use ElixirPhoenixWeb, :router

pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {ElixirPhoenixWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end

pipeline :api do
plug :accepts, ["json"]
end

scope "/", ElixirPhoenixWeb do
pipe_through :browser

get "/page", PageController, :home
post "/page", PageController, :home
put "/page", PageController, :home
patch "/page", PageController, :home
delete "/page", PageController, :home
socket "/socket", MyAppWeb.Socket, websocket: true, longpoll: false
end

# Other scopes may use custom stacks.
# scope "/api", ElixirPhoenixWeb do
# pipe_through :api
# end

# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:elixir_phoenix, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router

scope "/dev" do
pipe_through :browser

live_dashboard "/dashboard", metrics: ElixirPhoenixWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end
73 changes: 73 additions & 0 deletions spec/functional_test/fixtures/elixir_phoenix/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule ElixirPhoenix.MixProject do
use Mix.Project

def project do
[
app: :elixir_phoenix,
version: "0.1.0",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end

# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {ElixirPhoenix.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end

# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.7.1"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 3.3"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.18.16"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.7.2"},
{:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
{:swoosh, "~> 1.3"},
{:finch, "~> 0.13"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.20"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}
]
end

# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
"assets.build": ["tailwind default", "esbuild default"],
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
]
end
end
17 changes: 17 additions & 0 deletions spec/functional_test/testers/elixir_phoenix_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "../func_spec.cr"

extected_endpoints = [
Endpoint.new("/page", "GET"),
Endpoint.new("/page", "POST"),
Endpoint.new("/page", "PUT"),
Endpoint.new("/page", "PATCH"),
Endpoint.new("/page", "DELETE"),
Endpoint.new("/socket", "GET"),
Endpoint.new("/live", "GET"),
Endpoint.new("/phoenix/live_reload/socket", "GET"),
]

FunctionalTester.new("fixtures/elixir_phoenix/", {
:techs => 1,
:endpoints => 8,
}, extected_endpoints).test_all
1 change: 1 addition & 0 deletions src/analyzer/analyzer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def initialize_analyzers(logger : NoirLogger)
analyzers["java_jsp"] = ->analyzer_jsp(Hash(Symbol, String))
analyzers["c#-aspnet-mvc"] = ->analyzer_cs_aspnet_mvc(Hash(Symbol, String))
analyzers["rust_axum"] = ->analyzer_rust_axum(Hash(Symbol, String))
analyzers["elixir_phoenix"] = ->analyzer_elixir_phoenix(Hash(Symbol, String))

logger.info_sub "#{analyzers.size} Analyzers initialized"
logger.debug "Analyzers:"
Expand Down
64 changes: 64 additions & 0 deletions src/analyzer/analyzers/analyzer_elixir_phoenix.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require "../../models/analyzer"

class AnalyzerElixirPhoenix < Analyzer
def analyze
# Source Analysis
begin
Dir.glob("#{@base_path}/**/*") do |path|
next if File.directory?(path)
if File.exists?(path) && File.extname(path) == ".ex"
File.open(path, "r", encoding: "utf-8", invalid: :skip) do |file|
last_endpoint = Endpoint.new("", "")
file.each_line do |line|
endpoint = line_to_endpoint(line)
if endpoint.method != ""
@result << endpoint
last_endpoint = endpoint
_ = last_endpoint
end
end
end
end
end
rescue e
logger.debug e
end

@result
end

def line_to_endpoint(line : String) : Endpoint
line.scan(/get\s+['"](.+?)['"]\s*,\s*(.+?)\s*/) do |match|
@result << Endpoint.new("#{@url}#{match[1]}", "GET")
end

line.scan(/post\s+['"](.+?)['"]\s*,\s*(.+?)\s*/) do |match|
@result << Endpoint.new("#{@url}#{match[1]}", "POST")
end

line.scan(/patch\s+['"](.+?)['"]\s*,\s*(.+?)\s*/) do |match|
@result << Endpoint.new("#{@url}#{match[1]}", "PATCH")
end

line.scan(/put\s+['"](.+?)['"]\s*,\s*(.+?)\s*/) do |match|
@result << Endpoint.new("#{@url}#{match[1]}", "PUT")
end

line.scan(/delete\s+['"](.+?)['"]\s*,\s*(.+?)\s*/) do |match|
@result << Endpoint.new("#{@url}#{match[1]}", "DELETE")
end

line.scan(/socket\s+['"](.+?)['"]\s*,\s*(.+?)\s*/) do |match|
tmp = Endpoint.new("#{@url}#{match[1]}", "GET")
tmp.set_protocol("ws")
@result << tmp
end

Endpoint.new("", "")
end
end

def analyzer_elixir_phoenix(options : Hash(Symbol, String))
instance = AnalyzerElixirPhoenix.new(options)
instance.analyze
end
2 changes: 1 addition & 1 deletion src/detector/detector.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def detect_techs(base_path : String, options : Hash(Symbol, String), logger : No
DetectorPhpPure, DetectorPythonDjango, DetectorPythonFlask, DetectorPythonFastAPI,
DetectorRubyRails, DetectorRubySinatra, DetectorRubyHanami, DetectorOas2, DetectorOas3, DetectorRAML,
DetectorGoGin, DetectorKotlinSpring, DetectorJavaArmeria, DetectorCSharpAspNetMvc,
DetectorRustAxum,
DetectorRustAxum, DetectorElixirPhoenix,
])

channel = Channel(String).new
Expand Down
14 changes: 14 additions & 0 deletions src/detector/detectors/elixir_phoenix.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require "../../models/detector"

class DetectorElixirPhoenix < Detector
def detect(filename : String, file_contents : String) : Bool
check = file_contents.includes?("ElixirPhoenix")
check = check && filename.includes?("mix.exs")

check
end

def set_name
@name = "elixir_phoenix"
end
end
5 changes: 5 additions & 0 deletions src/techs/techs.cr
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ module NoirTechs
:framework => "Axum",
:similar => ["axum", "rust-axum", "rust_axum"],
},
:elixir_phoenix => {
:language => "Elixir",
:framework => "Phoenix",
:similar => ["phoenix", "elixir-phoenix", "elixir_phoenix"],
},
:oas2 => {
:format => ["JSON", "YAML"],
:similar => ["oas 2.0", "oas_2_0", "swagger 2.0", "swagger_2_0", "swagger"],
Expand Down