README
View Source
OeditusCredo
Custom Credo checks for detecting common Elixir/Phoenix anti-patterns, mistakes, and CWE Top 25 security vulnerabilities.
Overview
OeditusCredo provides 40 comprehensive custom Credo checks that detect common mistakes, risky code, and security vulnerabilities in Elixir and Phoenix projects:
Error Handling Anti-patterns
- MissingErrorHandling - Detects
{:ok, x} =pattern without error handling - SilentErrorCase - Detects case statements missing error branches
- SwallowingException - Detects try/rescue blocks without logging or re-raising
Database & Performance Issues
- InefficientFilter - Detects
Repo.allfollowed by Enum filtering - NPlusOneQuery - Detects potential N+1 queries (Enum.map with Repo calls)
- MissingPreload - Detects Ecto queries without proper preloading
LiveView & Concurrency Issues
- UnmanagedTask - Detects unsupervised
Task.asynccalls - SyncOverAsync - Detects blocking operations in LiveView/GenServer callbacks
- MissingHandleAsync - Detects blocking in handle_event without async pattern
- MissingThrottle - Detects form inputs without phx-debounce/throttle
- InlineJavascript - Detects inline JS handlers instead of phx-* bindings
Readability
- UnnecessaryInterpolatingSigil - Detects
~s/~c/~wwithout interpolation (suggests~S/~C/~W)
Code Quality & Maintainability
- DirectStructUpdate - Detects direct struct updates instead of changesets
- CallbackHell - Detects deeply nested case statements (suggests
with) - BlockingInPlug - Detects blocking operations in Plug functions
- UnsafeMapAccess - Detects bracket access on maps where dot access is safer (requires
typle+ Elixir >= 1.20)
Refactoring Suggestions
- SuggestFSM - Detects imperative status/state management (suggests
Finitomataor:gen_statem) - ChangeRiskAntiPatterns - Flags functions with a high CRAP score (both complex and under-tested). Opt-in/disabled by default; requires coverage data (see Change Risk Anti-Patterns (CRAP) score).
Telemetry & Observability
- MissingTelemetryInLiveViewMount - Detects LiveView mount/3 without telemetry events
- TelemetryInRecursiveFunction - Detects telemetry inside recursive functions (anti-pattern)
- MissingTelemetryInAuthPlug - Detects auth/authz plugs without telemetry
- MissingTelemetryForExternalHttp - Detects HTTP client calls without telemetry wrapper
Security - Injection (CWE-89, CWE-78, CWE-94, CWE-79)
- SQLInjection - Detects string interpolation/concatenation in Ecto queries
- OSCommandInjection - Detects user input passed to System.cmd/os:cmd
- CodeInjection - Detects dynamic code execution via Code.eval_string
- XSSVulnerability - Detects raw/1 with user input in templates
Security - Authentication & Authorization (CWE-306, CWE-862, CWE-863, CWE-639)
- MissingAuthentication - Detects controllers/routers without authentication plugs
- MissingAuthorization - Detects Phoenix actions without authorization checks
- IncorrectAuthorization - Detects role checks using negation/!= patterns
- InsecureDirectObjectReference - Detects direct DB lookups from user params without ownership checks
Security - Data Protection (CWE-200, CWE-798, CWE-502)
- SensitiveDataExposure - Detects sensitive fields in JSON responses and inspect output
- HardcodedCredentials - Detects hardcoded passwords, API keys, tokens, and secrets
- UnsafeDeserialization - Detects :erlang.binary_to_term without the :safe option
Security - Input & File Handling (CWE-20, CWE-22, CWE-434)
- ImproperInputValidation - Detects missing validation of external input
- PathTraversal - Detects user input in file paths without sanitization
- UnrestrictedFileUpload - Detects file uploads without content-type validation
Security - Web (CWE-352, CWE-918)
- MissingCSRFProtection - Detects API pipelines without CSRF protection
- SSRFVulnerability - Detects HTTP requests with user-controlled URLs
Security - Race Conditions (CWE-367)
- TOCTOU - Detects time-of-check/time-of-use patterns (File.exists? then File.read)
Important Note
All these checks are somewhat opinionated and might produce false positives. If a warning does not apply to your specific case, you can suppress it with # credo:disable-for-next-line or any other Credo config comment directive.
Installation
As a Project Dependency
Add oeditus_credo to your list of dependencies in mix.exs:
def deps do
[
{:oeditus_credo, "~> 0.1.0", only: [:dev, :test], runtime: false}
]
endStandalone Installation (No Dependency Required)
You can also use OeditusCredo without adding it to your project dependencies:
# Install as a Hex archive (recommended for development)
mix archive.install hex oeditus_credo
# Or download and use the escript executable (best for CI/CD)
curl -L https://github.com/Oeditus/oeditus_credo/releases/latest/download/oeditus_credo -o oeditus_credo
chmod +x oeditus_credo
See STANDALONE.md for detailed standalone usage instructions.
Usage
With Standalone Installation
If you installed OeditusCredo as an archive or escript:
mix oeditus_credo # Run with all checks enabled
mix oeditus_credo --strict # Fail on any issues
mix oeditus_credo lib/my_app # Analyze specific directory
With Project Dependency
Add the checks to your .credo.exs configuration:
%{
configs: [
%{
name: "default",
plugins: [],
requires: [],
checks: %{
enabled: [
# ... existing checks ...
# Error Handling
{OeditusCredo.Check.Warning.MissingErrorHandling, []},
{OeditusCredo.Check.Warning.SilentErrorCase, []},
{OeditusCredo.Check.Warning.SwallowingException, []},
# Database & Performance
{OeditusCredo.Check.Warning.InefficientFilter, []},
{OeditusCredo.Check.Warning.NPlusOneQuery, []},
{OeditusCredo.Check.Warning.MissingPreload, []},
# LiveView & Concurrency
{OeditusCredo.Check.Warning.UnmanagedTask, []},
{OeditusCredo.Check.Warning.SyncOverAsync, []},
{OeditusCredo.Check.Warning.MissingHandleAsync, []},
{OeditusCredo.Check.Warning.MissingThrottle, []},
{OeditusCredo.Check.Warning.InlineJavascript, []},
# Readability
{OeditusCredo.Check.Readability.UnnecessaryInterpolatingSigil, []},
# Code Quality
{OeditusCredo.Check.Warning.DirectStructUpdate, []},
{OeditusCredo.Check.Warning.CallbackHell, [max_nesting: 2]},
{OeditusCredo.Check.Warning.BlockingInPlug, []},
{OeditusCredo.Check.Warning.UnsafeMapAccess, []},
# Refactoring Suggestions
{OeditusCredo.Check.Refactoring.SuggestFSM, []},
# Telemetry & Observability
{OeditusCredo.Check.Warning.MissingTelemetryInLiveViewMount, []},
{OeditusCredo.Check.Warning.TelemetryInRecursiveFunction, []},
{OeditusCredo.Check.Warning.MissingTelemetryInAuthPlug, []},
{OeditusCredo.Check.Warning.MissingTelemetryForExternalHttp, []},
# Security - Injection
{OeditusCredo.Check.Security.SQLInjection, []},
{OeditusCredo.Check.Security.OSCommandInjection, []},
{OeditusCredo.Check.Security.CodeInjection, []},
{OeditusCredo.Check.Security.XSSVulnerability, []},
# Security - Auth
{OeditusCredo.Check.Security.MissingAuthentication, []},
{OeditusCredo.Check.Security.MissingAuthorization, []},
{OeditusCredo.Check.Security.IncorrectAuthorization, []},
{OeditusCredo.Check.Security.InsecureDirectObjectReference, []},
# Security - Data Protection
{OeditusCredo.Check.Security.SensitiveDataExposure, []},
{OeditusCredo.Check.Security.HardcodedCredentials, []},
{OeditusCredo.Check.Security.UnsafeDeserialization, []},
# Security - Input & File Handling
{OeditusCredo.Check.Security.ImproperInputValidation, []},
{OeditusCredo.Check.Security.PathTraversal, []},
{OeditusCredo.Check.Security.UnrestrictedFileUpload, []},
# Security - Web
{OeditusCredo.Check.Security.MissingCSRFProtection, []},
{OeditusCredo.Check.Security.SSRFVulnerability, []},
# Security - Race Conditions
{OeditusCredo.Check.Security.TOCTOU, []}
]
}
]
]
}Then run:
mix credo
Change Risk Anti-Patterns (CRAP) score
ChangeRiskAntiPatterns is opt-in and disabled by default because it needs
test-coverage data that Credo cannot produce on its own. It combines each
function's cyclomatic complexity with its test coverage:
CRAP = complexity^2 * (1 - coverage/100)^3 + complexityA fully covered function scores its complexity; a complex, untested function
scores much higher. The default maximum is 30 (the historical CRAP
convention).
IMPORTANT: This check only works when run after generating persisted coverage data. It reads
cover/default.coverdata, which is produced by--export-coverage:mix test --cover --export-coverage default mix credoPlain
mix test --coverprints a coverage report but does not leave an importable coverage file, so it is not sufficient. When no coverage data is found, the check does nothing by default (so it never breaks amix credorun launched without coverage). Setrequire_coverage: trueto turn missing coverage into a reported issue instead (useful in CI).
Enable it in .credo.exs:
{OeditusCredo.Check.Refactoring.ChangeRiskAntiPatterns, []}Parameters: max_score (default 30), coverdata (default
"cover/default.coverdata"), exclude_test_files (default true),
require_coverage (default false).
Configuration Options
All checks support configuration parameters. Pass them in .credo.exs:
General (Credo-standard) Parameters
Every check accepts the following general parameters provided by Credo:
false-- Disable a check entirely. When a check tuple usesfalseinstead of a keyword list, the check is skipped and produces no issues.# Disable a check {OeditusCredo.Check.Warning.NPlusOneQuery, false}exit_status(integer()) -- Override the exit status contributed by issues from this check. By default, all checks in the:warningcategory contribute exit status16. Settingexit_status: 0means the check still runs and reports issues, but they will not cause a non-zero exit code.# Run the check but don't fail CI on its issues {OeditusCredo.Check.Warning.NPlusOneQuery, exit_status: 0} # Custom exit status {OeditusCredo.Check.Security.SQLInjection, exit_status: 2}priority-- Override the base priority for the check (:low,:normal,:high,:higher, or:ignore).files-- Restrict which files the check runs on:{OeditusCredo.Check.Security.SQLInjection, files: %{included: ["lib/my_app/repo.ex"]}}
These parameters can be combined with any check-specific parameters.
Common Check-Specific Parameters
Every OeditusCredo check additionally accepts:
exclude_test_files(boolean(), default:false) -- When set totrue, files ending in_test.exsor located under a/test/directory are skipped.
Code Quality
- CallbackHell:
max_nesting-- Maximum allowed case nesting depth (default:2) - DirectStructUpdate:
extra_struct_patterns-- Additional regex strings for struct-like variable names (default:[]) - BlockingInPlug:
extra_blocking_modules-- Additional module atoms to treat as blocking (default:[])
Refactoring Suggestions
- SuggestFSM:
status_field_names-- Field names to watch (default:[:status, :state]);min_states-- Minimum distinct status values before flagging (default:3) - ChangeRiskAntiPatterns:
max_score-- Maximum CRAP score before a function is reported (default:30);coverdata-- Path to the persisted coverage file (default:"cover/default.coverdata");exclude_test_files-- Skip test files (default:true);require_coverage-- Report an issue when coverage data is missing instead of skipping (default:false). See Change Risk Anti-Patterns (CRAP) score for the required workflow.
LiveView & Concurrency
- SyncOverAsync:
extra_blocking_modules-- Additional blocking module atoms (default:[]);callback_functions-- Callback names to check (default:[:handle_event, :handle_call, :handle_info, :handle_cast, :handle_continue]) - MissingHandleAsync:
extra_blocking_modules-- Additional blocking module atoms (default:[])
Telemetry & Observability
- MissingTelemetryForExternalHttp:
extra_http_modules-- Additional{module_parts, [functions]}tuples (default:[]) - MissingTelemetryInAuthPlug:
extra_auth_plug_names-- Additional auth plug name substrings (default:[])
Security -- Injection
- CodeInjection:
extra_dangerous_functions-- AdditionalCode.*function atoms to flag (default:[])
Security -- Auth
- MissingAuthentication:
sensitive_actions-- Controller actions requiring auth (default:[:index, :show, :create, :new, :update, :edit, :delete, :destroy]) - MissingAuthorization:
extra_auth_indicators-- Additional authorization indicator substrings (default:[]) - IncorrectAuthorization:
extra_auth_indicators-- Additional authorization indicator substrings (default:[]) - InsecureDirectObjectReference:
extra_ownership_indicators-- Additional ownership/auth indicator substrings (default:[])
Security -- Data Protection
- SensitiveDataExposure:
extra_sensitive_terms-- Additional sensitive field substrings (default:[]) - HardcodedCredentials:
extra_credential_terms-- Additional credential name substrings (default:[])
Security -- Web
- SSRFVulnerability:
extra_http_modules-- Additional HTTP module atom lists, e.g.[[:MyHTTP]](default:[])
Example
# Customise check-specific params
{OeditusCredo.Check.Warning.CallbackHell, [max_nesting: 3]},
{OeditusCredo.Check.Warning.SyncOverAsync, [extra_blocking_modules: [:ExternalAPI]]},
{OeditusCredo.Check.Security.CodeInjection, [extra_dangerous_functions: [:compile_string]]},
{OeditusCredo.Check.Security.HardcodedCredentials, [exclude_test_files: true, extra_credential_terms: ["conn_string"]]},
{OeditusCredo.Check.Warning.MissingTelemetryForExternalHttp, [
extra_http_modules: [{[:MyApp, :HTTP], [:get, :post]}]
]},
# Run check as advisory only (won't affect exit code)
{OeditusCredo.Check.Warning.DirectStructUpdate, exit_status: 0},
# Disable a check entirely
{OeditusCredo.Check.Warning.InlineJavascript, false}Test Coverage
The library includes comprehensive tests for all 40 checks. Run tests with:
mix test
Run mix test to see the current test count and coverage.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License. See LICENSE for details.