diff --git a/.github/labeler.yml b/.github/labeler.yml
index c8d85e5b..7fd724ab 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -29,6 +29,12 @@
- src/output_builder/**
- src/models/output_builder.cr
+π·οΈ tagger:
+- changed-files:
+ - any-glob-to-any-file:
+ - src/taggers/**
+ - src/models/tag.cr
+
π spec:
- changed-files:
- any-glob-to-any-file: spec/**
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cda9a67f..1ddaae5e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- crystal-version: ['1.8.0', '1.9.0', '1.10.0', '1.11.0']
+ crystal-version: ['1.10.0', '1.11.0']
steps:
- uses: actions/checkout@v3
- uses: MeilCli/setup-crystal-action@v4
diff --git a/README.md b/README.md
index 3d60773f..36b8c566 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,25 @@
-
Noir is an attack surface detector that identifies endpoints by static analysis.
+
Attack surface detector that identifies endpoints by static analysis.
+
+
+
+
+
+
+
+
+
+
+ Key Features β’
+ Available Support Scope β’
+ Installation β’
+ Usage β’
+ Contributing
+
+
## Key Features
- Automatically identify language and framework from source code.
- Find API endpoints and web pages through code analysis.
@@ -10,7 +27,10 @@
- That provides structured data such as JSON and YAML for identified Attack Surfaces to enable seamless interaction with other tools. Also provides command line samples to easily integrate and collaborate with other tools, such as curls or httpie.
## Available Support Scope
-### Endpoint's Entities
+
+
+ Endpoint's Entities
+
- Path
- Method
- Param
@@ -19,7 +39,10 @@
- Protocol (e.g ws)
- Details (e.g The origin of the endpoint)
-### Languages and Frameworks
+
+
+
+ Languages and Frameworks
| Language | Framework | URL | Method | Param | Header | Cookie | WS |
|----------|-------------|-----|--------|-------|--------|--------|----|
@@ -45,8 +68,10 @@
| C# | ASP.NET MVC | β
| X | X | X | X | X |
| JS | Next | X | X | X | X | X | X |
+
-### Specification
+
+ Specification
| Specification | Format | URL | Method | Param | Header | WS |
|------------------------|---------|-----|--------|-------|--------|----|
@@ -55,6 +80,9 @@
| OAS 3.0 | JSON | β
| β
| β
| β
| X |
| OAS 3.0 | YAML | β
| β
| β
| β
| X |
| RAML | YAML | β
| β
| β
| β
| X |
+| HAR | JSON | β
| β
| β
| β
| X |
+
+
## Installation
### Homebrew (macOS)
@@ -115,6 +143,11 @@ Usage: noir
--no-color Disable color output
--no-log Displaying only the results
+ Tagger:
+ -T, --use-all-taggers Activates all taggers for full analysis coverage
+ --use-taggers VALUES Activates specific taggers (e.g., --use-taggers hunt,etc)
+ --list-taggers Lists all available taggers
+
Deliver:
--send-req Send results to a web request
--send-proxy http://proxy.. Send results to a web request via an HTTP proxy
@@ -140,54 +173,54 @@ Usage: noir
Example
```bash
-noir -b . -u https://testapp.internal.domains
+noir -b . -u https://testapp.internal.domains -T
```
-![](https://github.com/noir-cr/noir/assets/13212227/40d09acf-e250-4ea9-a84b-d9251a2d5147)
+![](https://github.com/noir-cr/noir/assets/13212227/4e69da04-d585-4745-9cc7-ef6e69e193b0)
JSON Result
```
-noir -b . -u https://testapp.internal.domains -f json
+noir -b . -u https://testapp.internal.domains -f json -T
```
```json
-[
- ...
- {
- "headers": [],
+{
+ "url": "https://testapp.internal.domains/query",
"method": "POST",
"params": [
{
- "name": "article_slug",
- "param_type": "json",
- "value": ""
- },
- {
- "name": "X-API-KEY",
- "value":"",
- "param_type":"header"
+ "name": "my_auth",
+ "value": "",
+ "param_type": "cookie",
+ "tags": []
},
{
- "name": "auth",
- "param_type": "cookie",
- "value": ""
+ "name": "query",
+ "value": "",
+ "param_type": "form",
+ "tags": [
+ {
+ "name": "sqli",
+ "description": "This parameter may be vulnerable to SQL Injection attacks.",
+ "tagger": "Hunt"
+ }
+ ]
}
],
- "protocol": "http",
- "url": "https://testapp.internal.domains/comments",
"details": {
"code_paths": [
{
- "path": "app_source/testapp.cr",
- "line": 3
+ "path": "spec/functional_test/fixtures/crystal_kemal/src/testapp.cr",
+ "line": 8
}
]
- }
+ },
+ "protocol": "http",
+ "tags": []
}
-]
```
-### Contributing
+## Contributing
Noir is open-source project and made it with β€οΈ
if you want contribute this project, please see [CONTRIBUTING.md](./CONTRIBUTING.md) and Pull-Request with cool your contents.
diff --git a/SECURITY.md b/SECURITY.md
index 034e8480..c98a5bdd 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,21 +1,19 @@
# Security Policy
-## Supported Versions
+## Reporting a Vulnerability
-Use this section to tell people about which versions of your project are
-currently being supported with security updates.
+Found a security issue? Let us know so we can fix it.
-| Version | Supported |
-| ------- | ------------------ |
-| 5.1.x | :white_check_mark: |
-| 5.0.x | :x: |
-| 4.0.x | :white_check_mark: |
-| < 4.0 | :x: |
+### How to Report
-## Reporting a Vulnerability
+* **For general security concerns**, please open a [GitHub issue](https://github.com/noir-cr/noir/issues). Use the `π‘οΈ security` label and describe the issue in as much detail as you can. This helps us to understand and address the problem more effectively.
+* **For sensitive matters**, we encourage you to directly email the [noir team members](https://github.com/orgs/noir-cr/people). Handling these issues discreetly is vital for everyone's safety.
+
+### Our Team
+
+Beyond being passionate open source contributors, we are also seasoned Red Team security engineers. Our dual expertise means we're not only ready but also keen to address any security issues you might identify. Consider us your approachable security allies. Whether you notice something minor or more significant, we encourage you to get in touch. Open dialogue is key to us, and we're here to address any security concerns you might haveβtogether.
-Use this section to tell people how to report a vulnerability.
+## Conclusion
+Your vigilance and willingness to report security issues are what help keep our project robust and secure. We appreciate the time and effort you put into making our community a safer place. Remember, no concern is too small; we're here to listen and act. Together, we can ensure a secure environment for all our users and contributors. Thank you for being an essential part of our project's security.
-Tell them where to go, how often they can expect to get an update on a
-reported vulnerability, what to expect if the vulnerability is accepted or
-declined, etc.
+Thank you for your support in maintaining the security and integrity of our project!
\ No newline at end of file
diff --git a/shard.lock b/shard.lock
index 3211fdbc..bdf4e9a2 100644
--- a/shard.lock
+++ b/shard.lock
@@ -4,6 +4,10 @@ shards:
git: https://github.com/mamantoha/crest.git
version: 1.3.11
+ har:
+ git: https://github.com/neuralegion/har.git
+ version: 1.2.0
+
http-client-digest_auth:
git: https://github.com/mamantoha/http-client-digest_auth.git
version: 0.6.0
diff --git a/shard.yml b/shard.yml
index e72751c7..bb2aa871 100644
--- a/shard.yml
+++ b/shard.yml
@@ -1,5 +1,5 @@
name: noir
-version: 0.13.0
+version: 0.14.0
authors:
- hahwul
@@ -12,7 +12,9 @@ targets:
dependencies:
crest:
github: mamantoha/crest
+ har:
+ github: NeuraLegion/har
-crystal: 1.8.2
+crystal: ~> 1.10
license: MIT
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 90ec266b..3ecae15f 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,10 +1,10 @@
name: noir
base: core20
-version: 0.13.0
+version: 0.14.0
summary: Attack surface detector that identifies endpoints by static analysis.
description: |
- Noir is your ally in the quest for digital fortification.
- A cutting-edge attack surface detector, it unveils hidden endpoints through meticulous static analysis.
+ Noir is an open-source project specializing in identifying attack surfaces for enhanced whitebox security testing and security pipeline.
+ This includes the capability to discover API endpoints, web endpoints, and other potential entry points within source code for thorough security analysis.
grade: stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
@@ -23,7 +23,7 @@ parts:
curl -fsSL https://crystal-lang.org/install.sh | sudo bash
snapcraftctl pull
shards install
- shards build --release
+ shards build --release --no-debug --production
cp ./bin/noir $SNAPCRAFT_PART_INSTALL/
snapcraftctl build
build-packages:
diff --git a/spec/functional_test/fixtures/crystal_kemal/custom_public/2.html b/spec/functional_test/fixtures/crystal_kemal/custom_public/2.html
new file mode 100644
index 00000000..e69de29b
diff --git a/spec/functional_test/fixtures/crystal_kemal/public/1.html b/spec/functional_test/fixtures/crystal_kemal/public/1.html
new file mode 100644
index 00000000..e69de29b
diff --git a/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr b/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr
index 787f08ac..0b9a46f2 100644
--- a/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr
+++ b/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr
@@ -10,8 +10,16 @@ post "/query" do
env.params.body["query"].as(String)
end
+get "/token" do
+ env.params.body["client_id"].as(String)
+ env.params.body["redirect_url"].as(String)
+ env.params.body["grant_type"].as(String)
+end
+
ws "/socket" do |socket|
socket.send "Hello from Kemal!"
end
+public_folder "custom_public"
+
Kemal.run
diff --git a/spec/functional_test/fixtures/har/example.har b/spec/functional_test/fixtures/har/example.har
new file mode 100644
index 00000000..89e6d01f
--- /dev/null
+++ b/spec/functional_test/fixtures/har/example.har
@@ -0,0 +1,153 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Firefox",
+ "version": "123.0.1"
+ },
+ "browser": {
+ "name": "Firefox",
+ "version": "123.0.1"
+ },
+ "pages": [
+ {
+ "id": "page_1",
+ "pageTimings": {
+ "onContentLoad": -1,
+ "onLoad": -1
+ },
+ "startedDateTime": "2024-03-17T00:17:31.653+09:00",
+ "title": "https://www.hahwul.com/"
+ }
+ ],
+ "entries": [
+ {
+ "startedDateTime": "2024-03-17T00:17:31.653+09:00",
+ "request": {
+ "bodySize": 0,
+ "method": "GET",
+ "url": "https://www.hahwul.com/",
+ "httpVersion": "HTTP/2",
+ "headers": [
+ {
+ "name": "Host",
+ "value": "www.hahwul.com"
+ },
+ {
+ "name": "User-Agent",
+ "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0"
+ },
+ {
+ "name": "Accept",
+ "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
+ }
+ ],
+ "cookies": [
+ {
+ "name": "_ga",
+ "value": "GA1.1.1310623768.1671977578"
+ },
+ {
+ "name": "_ga_N9SYSZ280B",
+ "value": "GS1.1.1710602187.53.0.1710602187.0.0.0"
+ }
+ ],
+ "queryString": [],
+ "headersSize": 690
+ },
+ "response": {
+ "status": 304,
+ "statusText": "",
+ "httpVersion": "HTTP/2",
+ "headers": [
+ {
+ "name": "date",
+ "value": "Sat, 16 Mar 2024 15:17:31 GMT"
+ },
+ {
+ "name": "via",
+ "value": "1.1 varnish"
+ },
+ {
+ "name": "cache-control",
+ "value": "max-age=600"
+ },
+ {
+ "name": "etag",
+ "value": "W/\"65f5937e-aadc\""
+ },
+ {
+ "name": "expires",
+ "value": "Sat, 16 Mar 2024 13:59:28 GMT"
+ },
+ {
+ "name": "age",
+ "value": "258"
+ },
+ {
+ "name": "x-served-by",
+ "value": "cache-icn1450027-ICN"
+ },
+ {
+ "name": "x-cache",
+ "value": "HIT"
+ },
+ {
+ "name": "x-cache-hits",
+ "value": "3"
+ },
+ {
+ "name": "x-timer",
+ "value": "S1710602252.651544,VS0,VE1"
+ },
+ {
+ "name": "vary",
+ "value": "Accept-Encoding"
+ },
+ {
+ "name": "x-fastly-request-id",
+ "value": "f40aba25bfeacafe5eb1bce11729a353344a30c3"
+ },
+ {
+ "name": "X-Firefox-Spdy",
+ "value": "h2"
+ }
+ ],
+ "cookies": [],
+ "content": {
+ "mimeType": "text/plain",
+ "size": 0,
+ "text": "\n\n......\n\n\n"
+ },
+ "redirectURL": "",
+ "headersSize": 382,
+ "bodySize": 382
+ },
+ "cache": {
+ "afterRequest": {
+ "expires": "1709911195",
+ "lastFetched": "1710602189",
+ "fetchCount": "2",
+ "_dataSize": "9437",
+ "_lastModified": "1710602191",
+ "_device": ""
+ }
+ },
+ "timings": {
+ "blocked": 0,
+ "dns": 0,
+ "connect": 0,
+ "ssl": 0,
+ "send": 0,
+ "wait": 7,
+ "receive": 0
+ },
+ "time": 7,
+ "_securityState": "secure",
+ "serverIPAddress": "185.199.108.153",
+ "connection": "443",
+ "pageref": "page_1"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/spec/functional_test/func_spec.cr b/spec/functional_test/func_spec.cr
index b5b13a73..a912e8fe 100644
--- a/spec/functional_test/func_spec.cr
+++ b/spec/functional_test/func_spec.cr
@@ -128,4 +128,8 @@ class FunctionalTester
def app
@app
end
+
+ def set_url(url)
+ @app.options[:url] = url
+ end
end
diff --git a/spec/functional_test/testers/crystal_kemal_spec.cr b/spec/functional_test/testers/crystal_kemal_spec.cr
index 5359e6fe..7da85e4c 100644
--- a/spec/functional_test/testers/crystal_kemal_spec.cr
+++ b/spec/functional_test/testers/crystal_kemal_spec.cr
@@ -7,9 +7,16 @@ extected_endpoints = [
Param.new("query", "", "form"),
Param.new("my_auth", "", "cookie"),
]),
+ Endpoint.new("/token", "GET", [
+ Param.new("grant_type", "", "form"),
+ Param.new("redirect_url", "", "form"),
+ Param.new("client_id", "", "form"),
+ ]),
+ Endpoint.new("/1.html", "GET"),
+ Endpoint.new("/2.html", "GET"),
]
FunctionalTester.new("fixtures/crystal_kemal/", {
:techs => 1,
- :endpoints => 3,
+ :endpoints => 6,
}, extected_endpoints).test_all
diff --git a/spec/functional_test/testers/har_spec.cr b/spec/functional_test/testers/har_spec.cr
new file mode 100644
index 00000000..2e71940e
--- /dev/null
+++ b/spec/functional_test/testers/har_spec.cr
@@ -0,0 +1,19 @@
+require "../func_spec.cr"
+
+extected_endpoints = [
+ Endpoint.new("https://www.hahwul.com/", "GET", [
+ Param.new("Host", "www.hahwul.com", "header"),
+ Param.new("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "header"),
+ Param.new("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "header"),
+ Param.new("_ga", "GA1.1.1310623768.1671977578", "cookie"),
+ Param.new("_ga_N9SYSZ280B", "GS1.1.1710602187.53.0.1710602187.0.0.0", "cookie"),
+ ]),
+]
+
+instance = FunctionalTester.new("fixtures/har/", {
+ :techs => 1,
+ :endpoints => 1,
+}, extected_endpoints)
+
+instance.set_url "https://www.hahwul.com"
+instance.test_all
diff --git a/spec/unit_test/tagger/tagger_spec.cr b/spec/unit_test/tagger/tagger_spec.cr
new file mode 100644
index 00000000..e2ea14b6
--- /dev/null
+++ b/spec/unit_test/tagger/tagger_spec.cr
@@ -0,0 +1,56 @@
+require "../../../src/tagger/tagger"
+
+describe "Tagger" do
+ it "hunt_tagger" do
+ noir_options = default_options()
+ extected_endpoints = [
+ Endpoint.new("/api/me", "GET", [
+ Param.new("q", "", "query"),
+ Param.new("query", "", "query"),
+ Param.new("filter", "", "query"),
+ Param.new("X-Forwarded-For", "", "header"),
+ ]),
+ Endpoint.new("/api/sign_ups", "POST", [
+ Param.new("url", "", "cookie"),
+ Param.new("command", "", "cookie"),
+ Param.new("role", "", "cookie"),
+ ]),
+ ]
+ NoirTaggers.run_tagger(extected_endpoints, noir_options, "hunt")
+ extected_endpoints.each do |endpoint|
+ endpoint.params.each do |param|
+ case param.name
+ when "query"
+ param.tags.each do |tag|
+ tag.name.should eq("sqli")
+ end
+ when "url"
+ param.tags.each do |tag|
+ tag.name.should eq("ssrf")
+ end
+ when "role"
+ param.tags.each do |tag|
+ tag.name.should eq("sqli")
+ end
+ end
+ end
+ end
+ end
+
+ it "oauth_tagger" do
+ noir_options = default_options()
+ extected_endpoints = [
+ Endpoint.new("/token", "GET", [
+ Param.new("client_id", "", "query"),
+ Param.new("grant_type", "", "query"),
+ Param.new("code", "", "query"),
+ ]),
+ ]
+ NoirTaggers.run_tagger(extected_endpoints, noir_options, "oauth")
+ extected_endpoints.each do |endpoint|
+ endpoint.tags.each do |tag|
+ tag.name.should eq("oauth")
+ end
+ end
+ end
+end
diff --git a/src/analyzer/analyzer.cr b/src/analyzer/analyzer.cr
index 09a82483..d6b09e9f 100644
--- a/src/analyzer/analyzer.cr
+++ b/src/analyzer/analyzer.cr
@@ -13,6 +13,7 @@ def initialize_analyzers(logger : NoirLogger)
analyzers["go_echo"] = ->analyzer_go_echo(Hash(Symbol, String))
analyzers["go_fiber"] = ->analyzer_go_fiber(Hash(Symbol, String))
analyzers["go_gin"] = ->analyzer_go_gin(Hash(Symbol, String))
+ analyzers["har"] = ->analyzer_har(Hash(Symbol, String))
analyzers["java_armeria"] = ->analyzer_armeria(Hash(Symbol, String))
analyzers["java_jsp"] = ->analyzer_jsp(Hash(Symbol, String))
analyzers["java_spring"] = ->analyzer_java_spring(Hash(Symbol, String))
diff --git a/src/analyzer/analyzers/analyzer_crystal_kemal.cr b/src/analyzer/analyzers/analyzer_crystal_kemal.cr
index 7914606f..f93a390a 100644
--- a/src/analyzer/analyzers/analyzer_crystal_kemal.cr
+++ b/src/analyzer/analyzers/analyzer_crystal_kemal.cr
@@ -2,6 +2,10 @@ require "../../models/analyzer"
class AnalyzerCrystalKemal < Analyzer
def analyze
+ # Variables
+ is_public = true
+ public_folders = [] of String
+
# Source Analysis
begin
Dir.glob("#{@base_path}/**/*") do |path|
@@ -24,6 +28,25 @@ class AnalyzerCrystalKemal < Analyzer
last_endpoint.push_param(param)
end
end
+
+ if line.includes? "serve_static false" || "serve_static(false)"
+ is_public = false
+ end
+
+ if line.includes? "public_folder"
+ begin
+ splited = line.split("public_folder")
+ public_folder = ""
+
+ if splited.size > 1
+ public_folder = splited[1].gsub("(", "").gsub(")", "").gsub(" ", "").gsub("\"", "").gsub("'", "")
+ if public_folder != ""
+ public_folders << public_folder
+ end
+ end
+ rescue
+ end
+ end
end
end
end
@@ -32,6 +55,29 @@ class AnalyzerCrystalKemal < Analyzer
logger.debug e
end
+ # Public Dir Analysis
+ if is_public
+ begin
+ Dir.glob("#{@base_path}/public/**/*") do |file|
+ next if File.directory?(file)
+ real_path = "#{@base_path}/public/".gsub(/\/+/, '/')
+ relative_path = file.sub(real_path, "")
+ @result << Endpoint.new("/#{relative_path}", "GET")
+ end
+
+ public_folders.each do |folder|
+ Dir.glob("#{@base_path}/#{folder}/**/*") do |file|
+ next if File.directory?(file)
+ relative_path = get_relative_path(@base_path, file)
+ relative_path = get_relative_path(folder, relative_path)
+ @result << Endpoint.new("/#{relative_path}", "GET")
+ end
+ end
+ rescue e
+ logger.debug e
+ end
+ end
+
result
end
diff --git a/src/analyzer/analyzers/analyzer_har.cr b/src/analyzer/analyzers/analyzer_har.cr
new file mode 100644
index 00000000..b4cf32a1
--- /dev/null
+++ b/src/analyzer/analyzers/analyzer_har.cr
@@ -0,0 +1,68 @@
+require "../../models/analyzer"
+
+class AnalyzerHar < Analyzer
+ def analyze
+ locator = CodeLocator.instance
+ har_files = locator.all("har-path")
+
+ if har_files.is_a?(Array(String)) && @url != ""
+ har_files.each do |har_file|
+ if File.exists?(har_file)
+ data = HAR.from_file(har_file)
+ logger.debug "Open #{har_file} file"
+ data.entries.each do |entry|
+ if entry.request.url.includes? @url
+ path = entry.request.url.to_s.gsub(@url, "")
+ endpoint = Endpoint.new(path, entry.request.method)
+
+ entry.request.query_string.each do |query|
+ endpoint.params << Param.new(query.name, query.value, "query")
+ end
+
+ is_websocket = false
+ entry.request.headers.each do |header|
+ endpoint.params << Param.new(header.name, header.value, "header")
+ if header.name == "Upgrade" && header.value == "websocket"
+ is_websocket = true
+ end
+ end
+
+ entry.request.cookies.each do |cookie|
+ endpoint.params << Param.new(cookie.name, cookie.value, "cookie")
+ end
+
+ post_data = entry.request.post_data
+ if post_data
+ params = post_data.params
+ mime_type = post_data.mime_type
+ param_type = "body"
+ if mime_type == "application/json"
+ param_type = "json"
+ end
+ if params
+ params.each do |param|
+ endpoint.params << Param.new(param.name, param.value.to_s, param_type)
+ end
+ end
+ end
+
+ details = Details.new(PathInfo.new(har_file, 0))
+ endpoint.set_details(details)
+ if is_websocket
+ endpoint.set_protocol "ws"
+ end
+ @result << endpoint
+ end
+ end
+ end
+ end
+ end
+
+ @result
+ end
+end
+
+def analyzer_har(options : Hash(Symbol, String))
+ instance = AnalyzerHar.new(options)
+ instance.analyze
+end
diff --git a/src/detector/detector.cr b/src/detector/detector.cr
index 33266ebd..37bcf295 100644
--- a/src/detector/detector.cr
+++ b/src/detector/detector.cr
@@ -22,6 +22,7 @@ def detect_techs(base_path : String, options : Hash(Symbol, String), logger : No
DetectorGoEcho,
DetectorGoFiber,
DetectorGoGin,
+ DetectorHar,
DetectorJavaArmeria,
DetectorJavaJsp,
DetectorJavaSpring,
diff --git a/src/detector/detectors/har.cr b/src/detector/detectors/har.cr
new file mode 100644
index 00000000..44cc53ae
--- /dev/null
+++ b/src/detector/detectors/har.cr
@@ -0,0 +1,29 @@
+require "../../models/detector"
+require "../../utils/json"
+require "../../utils/yaml"
+require "../../models/code_locator"
+require "har"
+
+class DetectorHar < Detector
+ def detect(filename : String, file_contents : String) : Bool
+ if (filename.includes? ".har") || (filename.includes? ".json")
+ if valid_json? file_contents
+ begin
+ data = HAR.from_string(file_contents)
+ if data.version.to_s.includes? "1."
+ locator = CodeLocator.instance
+ locator.push("har-path", filename)
+ return true
+ end
+ rescue
+ end
+ end
+ end
+
+ false
+ end
+
+ def set_name
+ @name = "har"
+ end
+end
diff --git a/src/models/endpoint.cr b/src/models/endpoint.cr
index a3838281..c79b7d73 100644
--- a/src/models/endpoint.cr
+++ b/src/models/endpoint.cr
@@ -4,26 +4,30 @@ require "yaml"
struct Endpoint
include JSON::Serializable
include YAML::Serializable
- property url, method, params, protocol, details
+ property url, method, params, protocol, details, tags
def initialize(@url : String, @method : String)
@params = [] of Param
@details = Details.new
@protocol = "http"
+ @tags = [] of Tag
end
def initialize(@url : String, @method : String, @details : Details)
@params = [] of Param
@protocol = "http"
+ @tags = [] of Tag
end
def initialize(@url : String, @method : String, @params : Array(Param))
@details = Details.new
@protocol = "http"
+ @tags = [] of Tag
end
def initialize(@url : String, @method : String, @params : Array(Param), @details : Details)
@protocol = "http"
+ @tags = [] of Tag
end
def set_details(@details : Details)
@@ -33,6 +37,10 @@ struct Endpoint
@protocol = protocol
end
+ def add_tag(tag : Tag)
+ @tags << tag
+ end
+
def push_param(param : Param)
@params << param
end
@@ -54,11 +62,16 @@ end
struct Param
include JSON::Serializable
include YAML::Serializable
- property name, value, param_type
+ property name, value, param_type, tags
# param_type can be "query", "json", "form", "header", "cookie"
def initialize(@name : String, @value : String, @param_type : String)
+ @tags = [] of Tag
+ end
+
+ def add_tag(tag : Tag)
+ @tags << tag
end
end
@@ -111,3 +124,12 @@ struct EndpointReference
def initialize(@endpoint : Endpoint, @metadata : Hash(Symbol, String))
end
end
+
+struct Tag
+ include JSON::Serializable
+ include YAML::Serializable
+ property name, description, tagger
+
+ def initialize(@name : String, @description : String, @tagger : String)
+ end
+end
diff --git a/src/models/noir.cr b/src/models/noir.cr
index fb0137b4..d2fb7ebf 100644
--- a/src/models/noir.cr
+++ b/src/models/noir.cr
@@ -1,5 +1,6 @@
require "../detector/detector.cr"
require "../analyzer/analyzer.cr"
+require "../tagger/tagger.cr"
require "../deliver/*"
require "../output_builder/*"
require "./endpoint.cr"
@@ -91,6 +92,22 @@ class NoirRunner
@endpoints = analysis_endpoints options, @techs, @logger
optimize_endpoints
combine_url_and_endpoints
+
+ # Run tagger
+ if @options[:all_taggers] == "yes"
+ @logger.info "Running all taggers."
+ NoirTaggers.run_tagger @endpoints, @options, "all"
+ if @is_debug
+ NoirTaggers.get_taggers.each do |tagger|
+ @logger.debug "Tagger: #{tagger}"
+ end
+ end
+ elsif @options[:use_taggers] != ""
+ @logger.info "Running #{@options[:use_taggers]} taggers."
+ NoirTaggers.run_tagger @endpoints, @options, @options[:use_taggers]
+ end
+
+ # Run deliver
deliver
end
diff --git a/src/models/output_builder.cr b/src/models/output_builder.cr
index fd9600c8..705009c1 100644
--- a/src/models/output_builder.cr
+++ b/src/models/output_builder.cr
@@ -38,6 +38,7 @@ class OutputBuilder
final_body = ""
final_headers = [] of String
final_cookies = [] of String
+ final_tags = [] of String
is_json = false
first_query = true
first_form = true
@@ -73,6 +74,12 @@ class OutputBuilder
if param.param_type == "json"
is_json = true
end
+
+ if param.tags.size > 0
+ param.tags.each do |tag|
+ final_tags << tag.name
+ end
+ end
end
if is_json
@@ -88,13 +95,19 @@ class OutputBuilder
end
end
- @logger.debug "Baked endpoint #{final_url} with #{final_body} body and #{final_headers.size} headers."
+ @logger.debug "Baked endpoints"
+ @logger.debug " + Final URL: #{final_url}"
+ @logger.debug " + Body: #{final_body}"
+ @logger.debug " + Headers: #{final_headers}"
+ @logger.debug " + Cookies: #{final_cookies}"
+ @logger.debug " + Tags: #{final_tags}"
{
url: final_url,
body: final_body,
header: final_headers,
cookie: final_cookies,
+ tags: final_tags.uniq,
body_type: is_json ? "json" : "form",
}
end
diff --git a/src/models/tagger.cr b/src/models/tagger.cr
new file mode 100644
index 00000000..51ab59bd
--- /dev/null
+++ b/src/models/tagger.cr
@@ -0,0 +1,30 @@
+require "./logger"
+
+class Tagger
+ @logger : NoirLogger
+ @options : Hash(Symbol, String)
+ @is_debug : Bool
+ @is_color : Bool
+ @is_log : Bool
+ @name : String
+
+ def initialize(options : Hash(Symbol, String))
+ @is_debug = str_to_bool(options[:debug])
+ @options = options
+ @is_color = str_to_bool(options[:color])
+ @is_log = str_to_bool(options[:nolog])
+ @name = ""
+
+ @logger = NoirLogger.new @is_debug, @is_color, @is_log
+ end
+
+ def name
+ @name
+ end
+
+ def perform(endpoints : Array(Endpoint)) : Array(Endpoint)
+ # After inheriting the class, write an action code here.
+
+ endpoints
+ end
+end
diff --git a/src/noir.cr b/src/noir.cr
index a1d09736..a3b6beb9 100644
--- a/src/noir.cr
+++ b/src/noir.cr
@@ -6,7 +6,7 @@ require "./options.cr"
require "./techs/techs.cr"
module Noir
- VERSION = "0.13.0"
+ VERSION = "0.14.0"
end
noir_options = default_options()
@@ -33,6 +33,21 @@ OptionParser.parse do |parser|
noir_options[:nolog] = "yes"
end
+ parser.separator "\n Tagger:".colorize(:blue)
+ parser.on "-T", "--use-all-taggers", "Activates all taggers for full analysis coverage" { |_| noir_options[:all_taggers] = "yes" }
+ parser.on "--use-taggers VALUES", "Activates specific taggers (e.g., --use-taggers hunt,oauth)" { |var| noir_options[:use_taggers] = var }
+ parser.on "--list-taggers", "Lists all available taggers" do
+ puts "Available taggers:"
+ techs = NoirTaggers.get_taggers
+ techs.each do |tagger, value|
+ puts " #{tagger.to_s.colorize(:green)}"
+ value.each do |k, v|
+ puts " #{k.to_s.colorize(:blue)}: #{v}"
+ end
+ end
+ exit
+ end
+
parser.separator "\n Deliver:".colorize(:blue)
parser.on "--send-req", "Send results to a web request" { |_| noir_options[:send_req] = "yes" }
parser.on "--send-proxy http://proxy..", "Send results to a web request via an HTTP proxy" { |var| noir_options[:send_proxy] = var }
diff --git a/src/options.cr b/src/options.cr
index f323d82c..764c7a29 100644
--- a/src/options.cr
+++ b/src/options.cr
@@ -19,6 +19,8 @@ def default_options
:url => "",
:use_filters => "",
:use_matchers => "",
+ :all_taggers => "no",
+ :use_taggers => "",
}
noir_options
diff --git a/src/output_builder/common.cr b/src/output_builder/common.cr
index a0ee7475..3ca81ac1 100644
--- a/src/output_builder/common.cr
+++ b/src/output_builder/common.cr
@@ -31,6 +31,16 @@ class OutputBuilderCommon < OutputBuilder
r_buffer += "\n β body: #{r_body}"
end
+ tags = baked[:tags]
+ endpoint.tags.each do |tag|
+ tags << tag.name.to_s
+ end
+
+ if tags.size > 0
+ r_tags = tags.join(" ").colorize(:light_magenta).toggle(@is_color)
+ r_buffer += "\n β tags: #{r_tags}"
+ end
+
if @options[:include_path] == "yes"
details = endpoint.details
if details.code_paths && details.code_paths.size > 0
diff --git a/src/tagger/tagger.cr b/src/tagger/tagger.cr
new file mode 100644
index 00000000..26030ff0
--- /dev/null
+++ b/src/tagger/tagger.cr
@@ -0,0 +1,43 @@
+require "./taggers/*"
+require "../models/tagger"
+
+module NoirTaggers
+ HasTaggers = {
+ hunt: {
+ name: "HuntParam Tagger",
+ desc: "Identifies common parameters vulnerable to certain vulnerability classes",
+ runner: HuntParamTagger,
+ },
+ oauth: {
+ name: "OAuth Tagger",
+ desc: "Identifies OAuth endpoints",
+ runner: OAuthTagger,
+ },
+ }
+
+ def self.get_taggers
+ HasTaggers
+ end
+
+ def self.run_tagger(endpoints : Array(Endpoint), options : Hash(Symbol, String), use_taggers : String)
+ tagger_list = [] of Tagger # This will hold instances of taggers
+
+ # Define taggers by creating instances
+ # Assuming HuntParamTagger is defined and is the only tagger
+ HasTaggers.each_value do |tagger|
+ if tagger[:runner].class.to_s == "Class"
+ instance = tagger[:runner].new(options)
+ tagger_list << instance
+ end
+ end
+
+ # Parsing use_taggers
+ use_taggers_arr = use_taggers.split(",")
+ use_taggers_arr = use_taggers_arr.map(&.strip)
+
+ # Run taggers
+ tagger_list.each do |tagger|
+ tagger.perform(endpoints) if use_taggers_arr.includes?(tagger.name) || use_taggers_arr.includes?("all")
+ end
+ end
+end
diff --git a/src/tagger/taggers/hunt_param.cr b/src/tagger/taggers/hunt_param.cr
new file mode 100644
index 00000000..c6486b42
--- /dev/null
+++ b/src/tagger/taggers/hunt_param.cr
@@ -0,0 +1,58 @@
+require "../../models/tagger"
+require "../../models/endpoint"
+
+class HuntParamTagger < Tagger
+ TAG_DEFINITIONS = {
+ "ssti" => {
+ "words" => ["template", "preview", "id", "view", "activity", "name", "content", "redirect"],
+ "description" => "This parameter may be vulnerable to Server Side Template Injection (SSTI) attacks.",
+ },
+ "ssrf" => {
+ "words" => ["dest", "redirect", "uri", "path", "continue", "url", "window", "next", "data", "reference", "site", "html", "val", "validate", "domain", "callback", "return", "page", "feed", "host", "port", "to", "out", "view", "dir", "show", "navigation", "open"],
+ "description" => "This parameter may be vulnerable to Server Side Request Forgery (SSRF) attacks.",
+ },
+ "sqli" => {
+ "words" => ["id", "select", "report", "role", "update", "query", "user", "name", "sort", "where", "search", "params", "process", "row", "view", "table", "from", "sel", "results", "sleep", "fetch", "order", "keyword", "column", "field", "delete", "string", "number", "filter"],
+ "description" => "This parameter may be vulnerable to SQL Injection attacks.",
+ },
+ "idor" => {
+ "words" => ["id", "user", "account", "number", "order", "no", "doc", "key", "email", "group", "profile", "edit", "report"],
+ "description" => "This parameter may be vulnerable to Insecure Direct Object Reference (IDOR) attacks.",
+ },
+ "file-inclusion" => {
+ "words" => ["file", "document", "folder", "root", "path", "pg", "style", "pdf", "template", "php_path", "doc"],
+ "description" => "This parameter may be vulnerable to File Inclusion attacks.",
+ },
+ "debug" => {
+ "words" => ["access", "admin", "dbg", "debug", "edit", "grant", "test", "alter", "clone", "create", "delete", "disable", "enable", "exec", "execute", "load", "make", "modify", "rename", "reset", "shell", "toggle", "adm", "root", "cfg", "config"],
+ "description" => "This parameter may be vulnerable to Debug method exploits.",
+ },
+ "command-injection" => {
+ "words" => ["daemon", "host", "upload", "dir", "execute", "download", "log", "ip", "cli", "cmd"],
+ "description" => "This parameter may be vulnerable to Command Injection attacks.",
+ },
+ }
+
+ def initialize(options : Hash(Symbol, String))
+ super
+ @name = "hunt"
+ end
+
+ def perform(endpoints : Array(Endpoint))
+ tagger = {} of String => Hash(String, Array(String) | String)
+ TAG_DEFINITIONS.each do |key, value|
+ tagger[key] = {"words" => value["words"], "description" => value["description"]}
+ end
+
+ endpoints.each do |endpoint|
+ endpoint.params.each do |param|
+ TAG_DEFINITIONS.each do |k, v|
+ if v["words"].includes? param.name
+ tag = Tag.new(k, v["description"].to_s, "Hunt")
+ param.add_tag(tag)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/src/tagger/taggers/oauth.cr b/src/tagger/taggers/oauth.cr
new file mode 100644
index 00000000..7697b7fd
--- /dev/null
+++ b/src/tagger/taggers/oauth.cr
@@ -0,0 +1,33 @@
+require "../../models/tagger"
+require "../../models/endpoint"
+
+class OAuthTagger < Tagger
+ WORDS = ["grant_type", "code", "redirect_uri", "redirect_url", "client_id", "client_secret"]
+
+ def initialize(options : Hash(Symbol, String))
+ super
+ @name = "oauth"
+ end
+
+ def perform(endpoints : Array(Endpoint))
+ endpoints.each do |endpoint|
+ tmp_params = [] of String
+
+ endpoint.params.each do |param|
+ tmp_params.push param.name.to_s
+ end
+
+ words_set = Set.new(WORDS)
+ tmp_params_set = Set.new(tmp_params)
+ intersection = words_set & tmp_params_set
+
+ # Check that at least three parameters match.
+ check = intersection.size.to_i >= 3
+
+ if check
+ tag = Tag.new("oauth", "Suspected OAuth endpoint for granting 3rd party access.", "Oauth")
+ endpoint.add_tag(tag)
+ end
+ end
+ end
+end
diff --git a/src/techs/techs.cr b/src/techs/techs.cr
index ed1bdf5b..ea543b85 100644
--- a/src/techs/techs.cr
+++ b/src/techs/techs.cr
@@ -5,6 +5,11 @@ module NoirTechs
:language => "Crystal",
:similar => ["kemal", "crystal-kemal", "crystal_kemal"],
},
+ :crystal_lucky => {
+ :framework => "Lucky",
+ :language => "Crystal",
+ :similar => ["lucky", "crystal-lucky", "crystal_lucky"],
+ },
:cs_aspnet_mvc => {
:framework => "ASP.NET MVC",
:language => "C#",
@@ -30,6 +35,10 @@ module NoirTechs
:language => "Go",
:similar => ["gin", "go-gin", "go_gin"],
},
+ :har => {
+ :format => ["JSON"],
+ :similar => ["har"],
+ },
:java_armeria => {
:framework => "Armeria",
:language => "Java",