Crystal-based attack surface detector that identifies endpoints by static analysis of source code across multiple languages and frameworks.
Reference these instructions first. Fallback to search or bash only when information here is outdated.
NEVER CANCEL builds or tests. Always use appropriate timeouts.
| Command | Alternative | Timeout |
|---|---|---|
just build |
shards build |
120s (~30s typical) |
just test |
crystal spec |
60s (~10s typical) |
just check |
format check + lint | 60s |
just fix |
auto-format + fix lint | 60s |
# Docker build (for CI or consistent environments)
docker run --rm -v $(pwd):/app -w /app crystallang/crystal:1.21.0-alpine sh -c "apk add --no-cache yaml-dev zstd-dev && shards install && shards build"
# Local install (Ubuntu/Debian)
curl -fsSL https://crystal-lang.org/install.sh | sudo bash
sudo apt install -y just./bin/noir -h # Help (includes all output formats)
./bin/noir --list-techs # List all supported technologies
./bin/noir --list-taggers # List available taggers
./bin/noir -b path/to/source # Basic analysis
./bin/noir -b . -f json # JSON output (see -h for all formats)
./bin/noir -b . --verbose # Detailed analysis
./bin/noir -b . -P # Passive security scan
./bin/noir -b . --send-proxy http://127.0.0.1:8080 # Forward to proxy (Burp/ZAP)
./bin/noir -b . --ai-provider openai --ai-model gpt-4 # AI-powered analysissrc/
├── analyzer/analyzers/ # Endpoint/parameter analyzers by language/framework
├── detector/detectors/ # Technology detection by language/framework
├── ext/ # External C/C++ bindings (e.g., Tree-sitter integration)
├── output_builder/ # Output format generation (JSON, YAML, OAS, etc.)
├── models/ # Data structures (includes delivers/, minilexer/)
├── llm/ # AI/LLM integration (general/, ollama/)
├── optimizer/ # Endpoint normalization/dedup and LLM optimizer
├── tagger/taggers/ # Endpoint tagging implementations
├── tagger/framework_taggers/ # Framework-specific auth taggers (by language)
├── deliver/ # Results delivery (proxy, elasticsearch)
├── minilexers/ # Custom lexers
├── miniparsers/ # Custom parsers
├── passive_scan/ # Passive security scanning
├── techs/ # Supported technologies catalog
├── utils/ # Utility functions
├── noir.cr # Main entry point
├── options.cr # CLI options parser
├── config_initializer.cr # Configuration initialization
├── completions.cr # Shell completion generation
└── banner.cr # Banner display
spec/
├── functional_test/
│ ├── fixtures/ # Sample code for testing (by language/framework)
│ └── testers/ # Functional test implementations
└── unit_test/ # Unit tests (mirrors src/ structure)
shard.yml- Dependencies and project metadatajustfile- Task definitions (just --listfor all commands).ameba.yml- Linting configuration.github/workflows/ci.yml- CI configuration
An analyzer is composed of three layers. Keep them separate — a framework adapter should not open files or re-implement parsing.
- Language Engine — shared per-language base in
src/analyzer/engines/{lang}_engine.cr. Owns file walking, concurrency, worker pool, file-content caching. - Route Extractor — shared per-language parser layer (
src/miniparsers/{lang}_route_extractor.cr). Takes source content, yields route declarations (method, path, location). No file I/O, no framework-specific rules. - Framework Adapter — thin per-framework class (
src/analyzer/analyzers/{lang}/{framework}.cr). Consumes routes from the extractor and applies framework-specific param mappings, filters, and special cases.
Rule: the framework adapter receives routes; it does not walk the filesystem or parse tokens itself.
Reference implementation: src/analyzer/analyzers/javascript/hono.cr on top of src/miniparsers/js_route_extractor.cr. Hono is ~205 lines because it follows this split; contrast with analyzers that inline all three responsibilities and grow to 500–800 lines.
Current coverage:
- Language engines: PHP, Ruby, Rust, Elixir, Swift, Crystal, Scala (Akka + Scalatra), JavaScript/TypeScript, Python, Go, Java, Kotlin, Perl. CSharp, Scala Play, and some others stay on the
Analyzerbase because their flows orchestrate multiple phases or carry self-contained extraction that doesn't share with other analyzers. - Route extractors:
- JavaScript/TypeScript:
js_route_extractor.cr(used by Hono, Express, Fastify, Koa, NestJS, Restify, AdonisJS, Elysia, Hapi, etc.) - High-fidelity Tree-sitter-based extractors (
*_route_extractor_ts.crand*_parameter_extractor_ts.cr) utilising vendored libtree-sitter bindings:- Go:
go_route_extractor_ts.cr - Java:
java_route_extractor_ts.cr,java_parameter_extractor_ts.cr(used by Spring, JAX-RS, Micronaut, etc.) - Kotlin:
kotlin_route_extractor_ts.cr,kotlin_ktor_route_extractor_ts.cr,kotlin_parameter_extractor_ts.cr(used by Spring, Ktor, etc.) - Python:
python_route_extractor_ts.cr(used by FastAPI, Flask, Django, etc.) - Framework-specific AST extractors:
adonisjs_extractor_ts.cr,elysia_extractor_ts.cr,hapi_extractor_ts.cr,http4k_extractor_ts.cr,jaxrs_extractor_ts.cr,micronaut_extractor_ts.cr,jvm_lambda_dsl_extractor_ts.cr
- Go:
- Traditional / Callee Extractors: Used as a fallback or framework-specific extraction across languages (e.g.,
cpp_callee_extractor.cr,crystal_callee_extractor.cr,go_callee_extractor.cr,js_callee_extractor.cr,ruby_callee_extractor.cr, etc.).
- JavaScript/TypeScript:
When adding a new framework in a language that already has an extractor, extend the extractor rather than re-parsing inline.
Two engine shapes — every engine exposes parallel_file_scan(&block) as a protected helper. Subclasses pick one of:
- Simple per-file: override
abstract def analyze_file(path) : Array(Endpoint). The engine's defaultanalyzedrives the walk and concats the returned endpoints. Used by Php/Rust/Swift/Crystal/Elixir/Scala analyzers. - Custom
analyze: overrideanalyzedirectly and callparallel_file_scanwhen you need closure state, a pre-phase (e.g., Express'sscan_for_router_mounts), or post-processing (e.g., Hono'sprocess_static_dirs, Amber/Kemal's public-dir pass). Used by Ruby/JavaScript/TypeScript analyzers and by the handful of Crystal/Elixir analyzers that override.
- Create
src/analyzer/analyzers/{language}/{framework}.cr— framework adapter only. Delegate parsing to the language's route extractor (see Analyzer Layering above). - Add functional test:
spec/functional_test/testers/{language}/{framework}_spec.cr - Add fixtures:
spec/functional_test/fixtures/{language}/{framework}/ - Register in
src/analyzer/analyzer.crif needed - Update
src/techs/techs.crwith technology metadata
- Create
src/detector/detectors/{language}/{framework}.cr - Add unit test:
spec/unit_test/detector/{language}/{framework}_detector_spec.cr - Register in
src/detector/detector.crif needed - Update
src/techs/techs.crwith technology metadata
- Create
src/output_builder/{format}_builder.cr - Add unit test:
spec/unit_test/output_builder/{format}_builder_spec.cr - Register in output builder selection logic
- Update
src/options.crhelp text
- Create
src/tagger/taggers/{tagger_name}.cr - Add unit test:
spec/unit_test/tagger/{tagger_name}_spec.cr - Register in
HasTaggersinsrc/tagger/tagger.cr
Framework taggers detect framework-specific patterns (e.g., auth decorators, middleware, guards) and tag endpoints accordingly. They extend FrameworkTagger < Tagger which provides file caching and read_source_context().
- Create
src/tagger/framework_taggers/{language}/{tagger_name}.cr- Inherit from
FrameworkTagger - Override
self.target_techsto return matching technology strings (e.g.,["python_django"]) - Override
perform(endpoints)to check and tag endpoints - Use
read_file(path)(cached) andread_source_context(endpoint)helpers
- Inherit from
- Add unit test:
spec/unit_test/tagger/framework_taggers/{tagger_name}_spec.cr - Add fixtures:
spec/functional_test/fixtures/{language}/{framework}_auth/ - Register in
HasFrameworkTaggersinsrc/tagger/tagger.cr
Key design notes:
FrameworkTaggerinherits fromTagger— shares@logger,@options,@name,perform()interface@file_cacheprevents redundant reads within a tagger run (pre-scan + per-endpoint checks)- Framework taggers are dispatched only when endpoints matching their
target_techsexist - Scope tracking (Go groups, Ktor authenticate blocks, Express app.use) uses heuristic brace counting — not AST-level, so edge cases with braces in strings/comments may occur
After any new component: run just test to validate.
just build- Ensure compilation succeedsjust test- Ensure all tests passcrystal tool format- Format code- Verify basic functionality:
./bin/noir -b spec/functional_test/fixtures/crystal
- Crystal ~> 1.19 (CI: 1.21.0)
- Docker image:
crystallang/crystal:1.21.0-alpine - Dependencies:
libyaml-dev,libzstd-dev,zlib1g-dev,pkg-config