Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
296 lines
12 KiB
Elixir
296 lines
12 KiB
Elixir
defmodule TarakanWeb.Router do
|
|
use TarakanWeb, :router
|
|
|
|
import TarakanWeb.AccountAuth
|
|
|
|
pipeline :browser do
|
|
plug :accepts, ["html"]
|
|
plug :fetch_session
|
|
plug :fetch_live_flash
|
|
plug :put_root_layout, html: {TarakanWeb.Layouts, :root}
|
|
plug TarakanWeb.CurrentPath
|
|
plug :protect_from_forgery
|
|
|
|
plug :put_secure_browser_headers, %{
|
|
"content-security-policy" =>
|
|
"default-src 'self'; base-uri 'self'; frame-ancestors 'self'; " <>
|
|
"object-src 'none'; img-src 'self' data: https:; " <>
|
|
"font-src 'self' data:; style-src 'self' 'unsafe-inline'; " <>
|
|
"script-src 'self'; connect-src 'self' wss: ws:; form-action 'self'",
|
|
"permissions-policy" => "camera=(), microphone=(), geolocation=()",
|
|
"x-frame-options" => "SAMEORIGIN"
|
|
}
|
|
|
|
plug TarakanWeb.Plugs.CodeBrowserHeaders
|
|
# CodeBrowserRateLimit runs after the session scope is fetched so
|
|
# admins/moderators are exempt from the per-IP budget.
|
|
plug :fetch_current_scope_for_account
|
|
plug TarakanWeb.Plugs.CodeBrowserRateLimit
|
|
end
|
|
|
|
# OAuth starts/callbacks each force an upstream authorization redirect or
|
|
# token-exchange POST; keep them on a small per-IP budget so a flood
|
|
# cannot proxy abuse to GitHub/GitLab.
|
|
pipeline :oauth_rate_limited do
|
|
plug :rate_limit_oauth_ip
|
|
end
|
|
|
|
pipeline :api do
|
|
plug :accepts, ["json"]
|
|
plug TarakanWeb.Plugs.ApiRateLimit, mode: :ip
|
|
end
|
|
|
|
# Stripe webhooks: JSON in, no session/CSRF (signature-verified instead).
|
|
pipeline :stripe_webhook do
|
|
plug :accepts, ["json"]
|
|
end
|
|
|
|
scope "/webhooks", TarakanWeb.Webhooks do
|
|
pipe_through :stripe_webhook
|
|
|
|
post "/stripe", StripeController, :handle
|
|
end
|
|
|
|
pipeline :api_authenticated do
|
|
plug :fetch_api_account
|
|
plug TarakanWeb.Plugs.ApiRateLimit, mode: :actor
|
|
end
|
|
|
|
pipeline :seo do
|
|
plug :accepts, ["xml", "txt"]
|
|
end
|
|
|
|
# Badges are embedded in READMEs on other hosts: no session, no CSRF, no
|
|
# browser security headers, and an IP budget because anyone can hotlink one.
|
|
pipeline :badge do
|
|
plug :accepts, ["svg", "html"]
|
|
# Its own bucket: a badge embedded in a popular README is high-volume and
|
|
# cheap (cached), and must not drain the API budget for the same network.
|
|
plug TarakanWeb.Plugs.ApiRateLimit, mode: :ip, bucket: :badge_ip, request_limit: 600
|
|
end
|
|
|
|
scope "/", TarakanWeb do
|
|
pipe_through :seo
|
|
|
|
get "/robots.txt", SEOController, :robots
|
|
get "/.well-known/security.txt", SEOController, :security_txt
|
|
get "/feeds/findings.xml", FeedController, :findings
|
|
get "/feeds/infestations.xml", FeedController, :infestations
|
|
get "/sitemap.xml", SEOController, :sitemap
|
|
get "/sitemap/hubs.xml", SEOController, :sitemap_hubs
|
|
get "/sitemap/repos/:page", SEOController, :sitemap_repos
|
|
get "/sitemap/findings/:page", SEOController, :sitemap_findings
|
|
get "/sitemap/jobs/:page", SEOController, :sitemap_jobs
|
|
end
|
|
|
|
scope "/", TarakanWeb do
|
|
pipe_through [:browser, :oauth_rate_limited]
|
|
|
|
get "/auth/github", GitHubAuthController, :request
|
|
get "/auth/github/callback", GitHubAuthController, :callback
|
|
get "/auth/gitlab", GitLabAuthController, :request
|
|
get "/auth/gitlab/callback", GitLabAuthController, :callback
|
|
end
|
|
|
|
scope "/", TarakanWeb do
|
|
pipe_through :browser
|
|
|
|
# Permanent redirects from the pre-rename /patterns URLs.
|
|
get "/patterns", InfestationRedirectController, :index
|
|
get "/patterns/:pattern_key", InfestationRedirectController, :show
|
|
|
|
live_session :public,
|
|
on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
|
|
live "/", RepositoryLive.Index, :index
|
|
live "/policies/disclosure", PageLive, :disclosure
|
|
live "/policies/content", PageLive, :content
|
|
live "/pricing", PageLive, :pricing
|
|
live "/services/disclosure", PageLive, :managed_disclosure
|
|
live "/explore", ExploreLive, :index
|
|
live "/infestations", InfestationLive.Index, :index
|
|
live "/infestations/:pattern_key", InfestationLive.Show, :show
|
|
live "/leaderboard", LeaderboardLive, :index
|
|
live "/models", ModelAnalyticsLive, :index
|
|
live "/jobs", JobsLive, :index
|
|
live "/jobs/:id", ReviewTaskLive.Show, :show
|
|
live "/agents", AgentsLive, :index
|
|
live "/bounties", BountyLive.Index, :index
|
|
# /bounties/new authenticates in-mount (it must precede the :public_id
|
|
# route, and the public session runs first for the other two).
|
|
live "/bounties/new", BountyLive.New, :new
|
|
live "/bounties/:public_id", BountyLive.Show, :show
|
|
live "/findings/:public_id", FindingLive.Show, :show
|
|
live "/findings/:finding_ref/code", RepositoryCodeLive, :finding
|
|
end
|
|
end
|
|
|
|
scope "/badges", TarakanWeb do
|
|
pipe_through :badge
|
|
|
|
# Hosted repositories are addressed without a host, remote ones with it.
|
|
get "/:owner/:name", BadgeController, :show
|
|
get "/:host/:owner/:name", BadgeController, :show
|
|
end
|
|
|
|
scope "/api", TarakanWeb.API do
|
|
pipe_through [:api]
|
|
|
|
post "/client-auth/start", ClientAuthController, :start
|
|
post "/client-auth/exchange", ClientAuthController, :exchange
|
|
|
|
# Public, read-only record (IP rate-limited; no token required)
|
|
get "/findings/:public_id", FindingController, :show
|
|
get "/infestations", InfestationController, :index
|
|
get "/infestations/:pattern_key", InfestationController, :show
|
|
get "/infestations/:pattern_key/vaccine.yaml", InfestationController, :vaccine
|
|
get "/leaderboard", LeaderboardController, :index
|
|
end
|
|
|
|
scope "/api", TarakanWeb.API do
|
|
pipe_through [:api, :api_authenticated]
|
|
|
|
get "/repositories", RepositoryController, :index
|
|
post "/repositories", RepositoryController, :create
|
|
delete "/client-auth/session", ClientAuthController, :revoke
|
|
|
|
# Detectors. Submitting one requires a worker that validated it against
|
|
# known instances; reading one is free and lives on the public scope.
|
|
post "/patterns/:code_pattern_key/rule", InfestationController, :put_rule
|
|
|
|
# Jobs
|
|
get "/jobs", WorkController, :queue
|
|
get "/jobs/:id", WorkController, :show
|
|
post "/jobs/:id/claim", WorkController, :claim
|
|
post "/jobs/:id/claim/renew", WorkController, :renew
|
|
delete "/jobs/:id/claim", WorkController, :release
|
|
post "/jobs/:id/complete", WorkController, :complete
|
|
get "/:host/:owner/:name/jobs", WorkController, :index
|
|
|
|
# Reports + findings + checks
|
|
get "/:host/:owner/:name/reports", ScanController, :index
|
|
get "/:host/:owner/:name/memory", ScanController, :memory
|
|
post "/:host/:owner/:name/reports", ScanController, :create
|
|
post "/:host/:owner/:name/findings/:public_id/check", ScanController, :finding_verdict
|
|
post "/:host/:owner/:name/findings/:public_id/severity", ScanController, :finding_severity
|
|
post "/:host/:owner/:name/findings/:public_id/embedding", ScanController, :finding_embedding
|
|
post "/:host/:owner/:name/reports/:id/check", ScanController, :verdict
|
|
end
|
|
|
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
|
if Application.compile_env(:tarakan, :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: TarakanWeb.Telemetry
|
|
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
|
end
|
|
end
|
|
|
|
## Authentication routes
|
|
|
|
scope "/", TarakanWeb do
|
|
pipe_through [:browser, :require_authenticated_account]
|
|
|
|
live_session :require_authenticated_account,
|
|
on_mount: [{TarakanWeb.AccountAuth, :require_authenticated}, TarakanWeb.CurrentPath] do
|
|
live "/accounts/settings", AccountLive.Settings, :edit
|
|
live "/accounts/settings/confirm-email/:token", AccountLive.Settings, :confirm_email
|
|
live "/accounts/settings/:section", AccountLive.Settings, :edit
|
|
live "/accounts/billing", BillingLive, :show
|
|
live "/alerts", AlertLive, :index
|
|
live "/client/authorize/:user_code", ClientAuthorizationLive, :show
|
|
live "/admin", AdminLive.Index, :index
|
|
live "/admin/accounts/:id", AdminLive.Show, :show
|
|
live "/repositories/new", RepositoryLive.New, :new
|
|
live "/moderation/report", ModerationReportLive.New, :new
|
|
live "/moderation/cases/:id", ModerationCaseLive.Show, :show
|
|
live "/moderation/queue", ModerationQueueLive.Index, :index
|
|
end
|
|
|
|
# Checkout/portal exit points: plain POSTs that 303 to Stripe.
|
|
post "/billing/checkout", BillingController, :checkout
|
|
post "/billing/portal", BillingController, :portal
|
|
|
|
post "/accounts/update-password", AccountSessionController, :update_password
|
|
end
|
|
|
|
scope "/", TarakanWeb do
|
|
pipe_through [:browser]
|
|
|
|
live_session :current_account,
|
|
on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
|
|
live "/accounts/register", AccountLive.Registration, :new
|
|
live "/accounts/log-in", AccountLive.Login, :new
|
|
live "/accounts/log-in/:token", AccountLive.Confirmation, :new
|
|
end
|
|
|
|
post "/accounts/log-in", AccountSessionController, :create
|
|
delete "/accounts/log-out", AccountSessionController, :delete
|
|
end
|
|
|
|
## Repository browsing
|
|
#
|
|
# Tarakan-hosted repositories live at GitHub-style bare paths
|
|
# (/handle/name); remote repositories lead with their host's domain
|
|
# (/github.com/owner/name, with legacy /github/... still resolving).
|
|
#
|
|
# Soundness: handles that would shadow a fixed route are reserved at
|
|
# registration (Tarakan.Accounts.Account), handles can never contain a
|
|
# dot, and host slugs are reserved handles - so a bare first segment is
|
|
# classifiable in the mounts via Tarakan.Hosts.host_segment?/1. The bare
|
|
# family is declared first so its literal segments win; when its first
|
|
# segment is actually a host, the mount reinterprets the params. That
|
|
# only happens for remote repositories literally named "code", "commits" or
|
|
# "security" (their security tab is the one known-degraded corner).
|
|
#
|
|
# These wildcard routes must stay at the bottom of the router so they can
|
|
# never shadow literal routes such as /accounts/log-in/:token.
|
|
scope "/", TarakanWeb do
|
|
pipe_through [:browser]
|
|
|
|
live_session :repository_browser,
|
|
on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
|
|
# A bare single segment is a contributor profile (GitHub-style
|
|
# /handle). Handles that would shadow a fixed route are reserved at
|
|
# registration, so this can never swallow /accounts, /jobs, etc.
|
|
live "/:handle", AccountLive.Profile, :show
|
|
|
|
live "/:owner/:name", RepositoryCodeLive, :entry
|
|
live "/:owner/:name/security", RepositoryLive.Show, :show
|
|
live "/:owner/:name/commits", RepositoryCommitsLive, :index
|
|
live "/:owner/:name/commits/:commit_sha", RepositoryCommitsLive, :show
|
|
live "/:owner/:name/code", RepositoryCodeLive, :code_entry
|
|
live "/:owner/:name/code/:commit_sha", RepositoryCodeLive, :show
|
|
live "/:owner/:name/code/:commit_sha/*path", RepositoryCodeLive, :show
|
|
|
|
live "/:host/:owner/:name", RepositoryCodeLive, :entry
|
|
live "/:host/:owner/:name/security", RepositoryLive.Show, :show
|
|
live "/:host/:owner/:name/commits", RepositoryCommitsLive, :index
|
|
live "/:host/:owner/:name/commits/:commit_sha", RepositoryCommitsLive, :show
|
|
live "/:host/:owner/:name/code", RepositoryCodeLive, :entry
|
|
live "/:host/:owner/:name/code/:commit_sha", RepositoryCodeLive, :show
|
|
live "/:host/:owner/:name/code/:commit_sha/*path", RepositoryCodeLive, :show
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def rate_limit_oauth_ip(conn, _opts) do
|
|
key = TarakanWeb.Plugs.ClientIp.remote_ip_bucket(conn)
|
|
|
|
if TarakanWeb.BrowserRateLimit.allowed?(:oauth_ip, key) do
|
|
conn
|
|
else
|
|
conn
|
|
|> put_resp_content_type("text/plain")
|
|
|> send_resp(429, "too many requests")
|
|
|> halt()
|
|
end
|
|
end
|
|
end
|