diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..3093620b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '🐞 bug' +assignees: hahwul, ksg97031 + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Run this '...' +2. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Versions** + - OS: [e.g. macos, linux] + - Version [e.g. v0.15.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..45b73f5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '💡 enhancement' +assignees: hahwul, ksg97031 + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/labeler.yml b/.github/labeler.yml index 7fd724ab..f153bf70 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,58 +1,48 @@ - +--- 🔬 analyzer: -- changed-files: - - any-glob-to-any-file: - - src/analyzer/** - - src/models/analyzer.cr - + - changed-files: + - any-glob-to-any-file: [src/analyzer/**, src/models/analyzer.cr] 💌 deliver: -- changed-files: - - any-glob-to-any-file: - - src/deliver/** - - src/models/deliver.cr - + - changed-files: + - any-glob-to-any-file: [src/deliver/**, src/models/deliver.cr] 🔎 detector: -- changed-files: - - any-glob-to-any-file: - - src/detector/** - - src/models/detector.cr - + - changed-files: + - any-glob-to-any-file: [src/detector/**, src/models/detector.cr] 🥢 mini-lexer: -- changed-files: - - any-glob-to-any-file: - - src/minilexers/** - - src/models/minilexer/** - + - changed-files: + - any-glob-to-any-file: + - src/minilexers/** + - src/models/minilexer/** + - src/miniparsers/** 📦 output-builder: -- changed-files: - - any-glob-to-any-file: - - src/output_builder/** - - src/models/output_builder.cr - + - changed-files: + - any-glob-to-any-file: + - src/output_builder/** + - src/models/output_builder.cr 🏷️ tagger: -- changed-files: - - any-glob-to-any-file: - - src/taggers/** - - src/models/tag.cr - + - changed-files: + - any-glob-to-any-file: [src/tagger/**, src/models/tag.cr] 💊 spec: -- changed-files: - - any-glob-to-any-file: spec/** - + - changed-files: + - any-glob-to-any-file: spec/** 🦺 github-action: -- changed-files: - - any-glob-to-any-file: .github/workflows/** - + - changed-files: + - any-glob-to-any-file: [.github/workflows/**, .github/labeler.yml] 📑 documentation: -- changed-files: - - any-glob-to-any-file: docs/** - + - changed-files: + - any-glob-to-any-file: + - docs/** + - README.md + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - LICENSE.md + - SECURITY.md ⚙️ options: -- changed-files: - - any-glob-to-any-file: src/options.cr - + - changed-files: + - any-glob-to-any-file: src/options.cr 🛥️ workflow: -- changed-files: - - any-glob-to-any-file: - - .github/workflows/** - - .github/labeler.yml \ No newline at end of file + - changed-files: + - any-glob-to-any-file: [.github/workflows/**, .github/labeler.yml] +🐳 docker: + - changed-files: + - any-glob-to-any-file: Dockerfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ddaae5e..20dfcc91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ --- -name: Crystal CI +name: CI on: - pull_request: + pull_request_target: branches: [main, dev] jobs: - build: + build-crystal: runs-on: ubuntu-latest strategy: - matrix: - crystal-version: ['1.10.0', '1.11.0'] + matrix: + crystal-version: [1.10.1, 1.11.2, 1.12.1] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: MeilCli/setup-crystal-action@v4 with: crystal_version: ${{ matrix.crystal-version }} @@ -18,6 +18,39 @@ jobs: run: shards install - name: Build run: shards build + build-docker: + runs-on: ubuntu-latest + strategy: + matrix: + arch: [linux/amd64, linux/arm64] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v3.1.1 + with: + cosign-release: v2.1.1 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + - name: Build Docker image + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ matrix.arch }} + cache-from: type=gha + cache-to: type=gha,mode=max lint: runs-on: ubuntu-latest container: diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 48c70bd4..7a962a0a 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -1,14 +1,14 @@ +--- name: Contributors on: push: - branches: - - main + branches: [main] workflow_dispatch: - inputs: - logLevel: - description: 'manual run' - required: false - default: '' + inputs: + logLevel: + description: manual run + required: false + default: '' jobs: contributors: runs-on: ubuntu-latest @@ -16,4 +16,4 @@ jobs: - uses: wow-actions/contributors-list@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - round: true \ No newline at end of file + round: true diff --git a/.github/workflows/ghcr_publish.yml b/.github/workflows/ghcr_publish.yml index 0214fdbd..21b21479 100644 --- a/.github/workflows/ghcr_publish.yml +++ b/.github/workflows/ghcr_publish.yml @@ -1,38 +1,33 @@ +--- name: GHCR Publish - # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. - on: push: - branches: [ "main" ] - tags: [ 'v*.*.*' ] - pull_request: - branches: [ "main" ] - + branches: [main, dev] + tags: [v*.*.*] env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} - - jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + arch: [linux/amd64, linux/arm64] permissions: contents: read packages: write # This is used to complete the identity challenge # with sigstore/fulcio when running outside of PRs. id-token: write - steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Install the cosign tool except on PR # https://github.com/sigstore/cosign-installer @@ -40,17 +35,22 @@ jobs: if: github.event_name != 'pull_request' uses: sigstore/cosign-installer@v3.1.1 with: - cosign-release: 'v2.1.1' + cosign-release: v2.1.1 + + # Using QEME for multiple platforms + # https://github.com/docker/build-push-action?tab=readme-ov-file#usage + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 # Workaround: https://github.com/docker/build-push-action/issues/461 - name: Setup Docker buildx - uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + uses: docker/setup-buildx-action@v3 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -60,7 +60,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -71,8 +71,9 @@ jobs: uses: docker/build-push-action@v5 with: context: . - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ matrix.arch }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/homebrew_publish.yml b/.github/workflows/homebrew_publish.yml index 33e13223..093ac3e3 100644 --- a/.github/workflows/homebrew_publish.yml +++ b/.github/workflows/homebrew_publish.yml @@ -1,9 +1,8 @@ +--- name: Homebrew tab Publish - on: release: types: [published] - jobs: homebrew-releaser: runs-on: ubuntu-latest @@ -15,21 +14,16 @@ jobs: homebrew_owner: noir-cr homebrew_tap: homebrew-noir formula_folder: Formula - github_token: ${{ secrets.NOIR_PUBLISH_TOKEN }} - commit_owner: hahwul commit_email: hahwul@gmail.com - depends_on: | - "crystal" - + "crystal" install: | - system "shards install" - system "shards build --release --no-debug --production" - bin.install "bin/noir" - - test: 'system "{bin}/noir", "-v"' + system "shards install" + system "shards build --release --no-debug --production" + bin.install "bin/noir" + test: system "{bin}/noir", "-v" update_readme_table: true skip_commit: false - debug: false \ No newline at end of file + debug: false diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 7adc272e..8b021510 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,7 +1,6 @@ -name: "Pull Request Labeler" -on: -- pull_request - +--- +name: Pull Request Labeler +on: [pull_request_target] jobs: labeler: permissions: @@ -9,4 +8,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 \ No newline at end of file + - uses: actions/labeler@v5 diff --git a/.github/workflows/snapcraft_publish.yml b/.github/workflows/snapcraft_publish.yml index cbce7c1b..000b6100 100644 --- a/.github/workflows/snapcraft_publish.yml +++ b/.github/workflows/snapcraft_publish.yml @@ -1,18 +1,16 @@ +--- name: Snapcraft tab Publish - on: release: types: [published] - workflow_dispatch: inputs: logLevel: - description: 'Log level' + description: Log level required: true - default: 'warning' + default: warning tags: - description: 'Test scenario tags' - + description: Test scenario tags jobs: snapcraft-releaser: runs-on: ubuntu-latest @@ -21,21 +19,18 @@ jobs: fail-fast: false matrix: platform: - - amd64 + - amd64 #- arm64 steps: - name: Check out Git repository uses: actions/checkout@v3 - - uses: diddlesnaps/snapcraft-multiarch-action@v1 with: architecture: ${{ matrix.platform }} id: build - - uses: diddlesnaps/snapcraft-review-action@v1 with: snap: ${{ steps.build.outputs.snap }} - - uses: snapcore/action-publish@master env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }} diff --git a/Dockerfile b/Dockerfile index 227c6be7..094c823b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,14 @@ FROM crystallang/crystal:latest-alpine As builder WORKDIR /noir COPY . . -RUN shards install -RUN shards build --release --no-debug --production +RUN shards install --production +RUN shards build --release --production --static --no-debug # RUNNER -FROM crystallang/crystal:latest-alpine As runner +FROM alpine +USER 2:2 + COPY --from=builder /noir/bin/noir /usr/local/bin/noir +COPY --from=builder /etc/ssl/cert.pem /etc/ssl/ + CMD ["noir"] diff --git a/README.md b/README.md index 36b8c566..e173f632 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ |----------|-------------|-----|--------|-------|--------|--------|----| | Crystal | Kemal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Crystal | Lucky | ✅ | ✅ | ✅ | ✅ | ✅ | X | +| Go | Beego | ✅ | ✅ | X | X | X | X | | Go | Echo | ✅ | ✅ | ✅ | ✅ | ✅ | X | | Go | Gin | ✅ | ✅ | ✅ | ✅ | ✅ | X | | Go | Fiber | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | @@ -63,7 +64,9 @@ | Java | Spring | ✅ | ✅ | X | X | X | X | | Kotlin | Spring | ✅ | ✅ | ✅ | X | X | X | | JS | Express | ✅ | ✅ | ✅ | ✅ | ✅ | X | +| JS | Restify | ✅ | ✅ | ✅ | ✅ | ✅ | X | | Rust | Axum | ✅ | ✅ | X | X | X | X | +| Rust | Rocket | ✅ | ✅ | X | X | X | X | | Elixir | Phoenix | ✅ | ✅ | X | X | X | ✅ | | C# | ASP.NET MVC | ✅ | X | X | X | X | X | | JS | Next | X | X | X | X | X | X | diff --git a/shard.lock b/shard.lock index bdf4e9a2..d0b3cf71 100644 --- a/shard.lock +++ b/shard.lock @@ -2,7 +2,7 @@ version: 2.0 shards: crest: git: https://github.com/mamantoha/crest.git - version: 1.3.11 + version: 1.3.13 har: git: https://github.com/neuralegion/har.git @@ -14,5 +14,5 @@ shards: http_proxy: git: https://github.com/mamantoha/http_proxy.git - version: 0.10.1 + version: 0.10.3 diff --git a/shard.yml b/shard.yml index bb2aa871..0db2be34 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: noir -version: 0.14.0 +version: 0.15.0 authors: - hahwul @@ -12,6 +12,7 @@ targets: dependencies: crest: github: mamantoha/crest + version: ~> 1.3.13 har: github: NeuraLegion/har diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3ecae15f..947637fc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: noir base: core20 -version: 0.14.0 +version: 0.15.0 summary: Attack surface detector that identifies endpoints by static analysis. description: | Noir is an open-source project specializing in identifying attack surfaces for enhanced whitebox security testing and security pipeline. diff --git a/spec/functional_test/fixtures/go_beego/go.mod b/spec/functional_test/fixtures/go_beego/go.mod new file mode 100644 index 00000000..b1702a47 --- /dev/null +++ b/spec/functional_test/fixtures/go_beego/go.mod @@ -0,0 +1,5 @@ +module github.com/hahwul/test-go-app + +go 1.22.2 + +require github.com/beego/beego/v2 v2.2.1 // indirect \ No newline at end of file diff --git a/spec/functional_test/fixtures/go_beego/server.go b/spec/functional_test/fixtures/go_beego/server.go new file mode 100644 index 00000000..9e78b1c6 --- /dev/null +++ b/spec/functional_test/fixtures/go_beego/server.go @@ -0,0 +1,21 @@ +package main + +import ( + "context" + + "github.com/beego/beego/v2/server/web" +) + +func main() { + web.Run() + + web.Get("/", func(ctx *context.Context) { + ctx.Output.Body([]byte("hello world")) + }) + + web.Post("/alice", func(ctx *context.Context) { + ctx.GetCookie("auth") + ctx.GetStrings("query") + ctx.Output.Body([]byte("bob")) + }) +} diff --git a/spec/functional_test/fixtures/java_spring/src/ItemController.java b/spec/functional_test/fixtures/java_spring/src/ItemController.java index 0ad6063f..0ae7c25b 100644 --- a/spec/functional_test/fixtures/java_spring/src/ItemController.java +++ b/spec/functional_test/fixtures/java_spring/src/ItemController.java @@ -26,6 +26,10 @@ public void deleteItem(@PathVariable Long id) { @GetMapping("/json/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) public void getItemJson(){ } + + @RequestMapping("/multiple/methods", method = {RequestMethod.GET, RequestMethod.POST}) + public void multipleMethods(){ + } } class Item { diff --git a/spec/functional_test/fixtures/js_restify/app.js b/spec/functional_test/fixtures/js_restify/app.js new file mode 100644 index 00000000..5166fc0e --- /dev/null +++ b/spec/functional_test/fixtures/js_restify/app.js @@ -0,0 +1,32 @@ +const restify = require('restify'); + +function setupServer(server) { + server.get('/', function(req, res, next) { + var userAgent = req.header('X-API-Key'); + var paramName = req.query.name; + + res.send('index'); // Assuming 'index' is a simple response for demonstration + return next(); + }); + + server.post('/upload', function(req, res, next) { + // Restify does not parse cookies by default, so you need to use a plugin or middleware if you want to access cookies + // const auth = req.cookies.auth; // This line needs adjustment if using cookies + const name = req.body.name; + + res.send('index'); // Similarly, adjust according to your actual response handling + return next(); + }); +} + +// Setup Restify server +const server = restify.createServer(); + +server.use(restify.plugins.bodyParser()); // Parse JSON body data +// server.use(restify.plugins.cookieParser()); // Uncomment if you need cookie parsing + +setupServer(server); + +server.listen(3000, function() { + console.log('%s listening at %s', server.name, server.url); +}); diff --git a/spec/functional_test/fixtures/rust_rocket/Cargo.toml b/spec/functional_test/fixtures/rust_rocket/Cargo.toml new file mode 100644 index 00000000..0036235a --- /dev/null +++ b/spec/functional_test/fixtures/rust_rocket/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "example-hello-world" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +schemars = "0.8" +okapi = { version = "0.6.0-alpha-1" } +rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger", "secrets"] } +dotenv = "0.15.0" +mongodb = "2.1.0" +futures = "0.3" +chrono = "0.4" \ No newline at end of file diff --git a/spec/functional_test/fixtures/rust_rocket/src/main.rs b/spec/functional_test/fixtures/rust_rocket/src/main.rs new file mode 100644 index 00000000..68debe06 --- /dev/null +++ b/spec/functional_test/fixtures/rust_rocket/src/main.rs @@ -0,0 +1,17 @@ +#[macro_use] extern crate rocket; + +#[get("/")] +fn index() -> &'static str { + "Hello, world!" +} + +#[post("/customer", data = "")] +fn customer() -> &'static str { + "Hello, world!" +} + +#[launch] +fn rocket() -> _ { + rocket::build().mount("/", routes![index]) + .mount("/public", FileServer::from("/public_dir")) +} \ No newline at end of file diff --git a/spec/functional_test/func_spec.cr b/spec/functional_test/func_spec.cr index a912e8fe..6728269d 100644 --- a/spec/functional_test/func_spec.cr +++ b/spec/functional_test/func_spec.cr @@ -78,7 +78,7 @@ class FunctionalTester key = endpoint.method.to_s + "::" + endpoint.url.to_s found_endpoint = find_endpoint key if found_endpoint.nil? - it "endpoint [#{key}] not founded" do + it "endpoint [#{key}] not found" do false.should eq true end else diff --git a/spec/functional_test/testers/go_beego_spec.cr b/spec/functional_test/testers/go_beego_spec.cr new file mode 100644 index 00000000..a22c20f6 --- /dev/null +++ b/spec/functional_test/testers/go_beego_spec.cr @@ -0,0 +1,14 @@ +require "../func_spec.cr" + +extected_endpoints = [ + Endpoint.new("/alice", "POST", [ + Param.new("query", "", "query"), + Param.new("auth", "", "cookie"), + ]), + Endpoint.new("/", "GET"), +] + +FunctionalTester.new("fixtures/go_beego/", { + :techs => 1, + :endpoints => 2, +}, extected_endpoints).test_all diff --git a/spec/functional_test/testers/java_spring_spec.cr b/spec/functional_test/testers/java_spring_spec.cr index 04cfa8ed..1e0c23e4 100644 --- a/spec/functional_test/testers/java_spring_spec.cr +++ b/spec/functional_test/testers/java_spring_spec.cr @@ -19,6 +19,8 @@ extected_endpoints = [ Endpoint.new("/items", "POST", [Param.new("id", "", "form"), Param.new("name", "", "form")]), Endpoint.new("/items/update/{id}", "PUT", [Param.new("id", "", "json"), Param.new("name", "", "json")]), Endpoint.new("/items/delete/{id}", "DELETE"), + Endpoint.new("/items/multiple/methods", "GET"), + Endpoint.new("/items/multiple/methods", "POST"), Endpoint.new("/greet", "GET", [ Param.new("name", "", "query"), Param.new("header", "", "header"), @@ -32,5 +34,5 @@ extected_endpoints = [ FunctionalTester.new("fixtures/java_spring/", { :techs => 1, - :endpoints => 17, + :endpoints => 19, }, extected_endpoints).test_all diff --git a/spec/functional_test/testers/js_restify_spec.cr b/spec/functional_test/testers/js_restify_spec.cr new file mode 100644 index 00000000..8af6a52d --- /dev/null +++ b/spec/functional_test/testers/js_restify_spec.cr @@ -0,0 +1,17 @@ +require "../func_spec.cr" + +extected_endpoints = [ + Endpoint.new("/", "GET", [ + Param.new("name", "", "query"), + Param.new("X-API-Key", "", "header"), + ]), + Endpoint.new("/upload", "POST", [ + Param.new("name", "", "json"), + Param.new("auth", "", "cookie"), + ]), +] + +FunctionalTester.new("fixtures/js_restify/", { + :techs => 1, + :endpoints => 2, +}, extected_endpoints).test_all diff --git a/spec/functional_test/testers/rust_rocket_spec.cr b/spec/functional_test/testers/rust_rocket_spec.cr new file mode 100644 index 00000000..fb0f1efd --- /dev/null +++ b/spec/functional_test/testers/rust_rocket_spec.cr @@ -0,0 +1,11 @@ +require "../func_spec.cr" + +extected_endpoints = [ + Endpoint.new("/", "GET"), + Endpoint.new("/customer", "POST"), +] + +FunctionalTester.new("fixtures/rust_rocket/", { + :techs => 1, + :endpoints => 2, +}, extected_endpoints).test_all diff --git a/spec/unit_test/detector/detect_js_restify_spec.cr b/spec/unit_test/detector/detect_js_restify_spec.cr new file mode 100644 index 00000000..d0f296fc --- /dev/null +++ b/spec/unit_test/detector/detect_js_restify_spec.cr @@ -0,0 +1,13 @@ +require "../../../src/detector/detectors/*" + +describe "Detect JS Restify" do + options = default_options() + instance = DetectorJsRestify.new options + + it "require_single_quot" do + instance.detect("index.js", "require('restify')").should eq(true) + end + it "require_double_quot" do + instance.detect("index.js", "require(\"restify\")").should eq(true) + end +end diff --git a/spec/unit_test/tagger/tagger_spec.cr b/spec/unit_test/tagger/tagger_spec.cr index e2ea14b6..40260866 100644 --- a/spec/unit_test/tagger/tagger_spec.cr +++ b/spec/unit_test/tagger/tagger_spec.cr @@ -21,14 +21,17 @@ describe "Tagger" do endpoint.params.each do |param| case param.name when "query" + param.tags.empty?.should be_false param.tags.each do |tag| tag.name.should eq("sqli") end when "url" + param.tags.empty?.should be_false param.tags.each do |tag| tag.name.should eq("ssrf") end when "role" + param.tags.empty?.should be_false param.tags.each do |tag| tag.name.should eq("sqli") end @@ -48,9 +51,76 @@ describe "Tagger" do ] NoirTaggers.run_tagger(extected_endpoints, noir_options, "oauth") extected_endpoints.each do |endpoint| + endpoint.tags.empty?.should be_false endpoint.tags.each do |tag| tag.name.should eq("oauth") end end end + + it "cors_tagger" do + noir_options = default_options() + extected_endpoints = [ + Endpoint.new("/api/me", "GET", [ + Param.new("q", "", "query"), + Param.new("Origin", "", "header"), + ]), + ] + NoirTaggers.run_tagger(extected_endpoints, noir_options, "cors") + extected_endpoints.each do |endpoint| + endpoint.tags.empty?.should be_false + endpoint.tags.each do |tag| + tag.name.should eq("cors") + end + end + end + + it "soap_tagger" do + noir_options = default_options() + extected_endpoints = [ + Endpoint.new("/api/me", "GET", [ + Param.new("SOAPAction", "", "header"), + ]), + ] + NoirTaggers.run_tagger(extected_endpoints, noir_options, "soap") + extected_endpoints.each do |endpoint| + endpoint.tags.empty?.should be_false + endpoint.tags.each do |tag| + tag.name.should eq("soap") + end + end + end + + it "websocket_tagger_1" do + noir_options = default_options() + extected_endpoints = [ + Endpoint.new("/ws", "GET", [ + Param.new("sec-websocket-version", "", "header"), + Param.new("Sec-WebSocket-Key", "", "header"), + ]), + ] + NoirTaggers.run_tagger(extected_endpoints, noir_options, "websocket") + extected_endpoints.each do |endpoint| + endpoint.tags.empty?.should be_false + endpoint.tags.each do |tag| + tag.name.should eq("websocket") + end + end + end + + it "websocket_tagger_2" do + noir_options = default_options() + e = Endpoint.new("/ws", "GET") + e.set_protocol("ws") + + extected_endpoints = [e] + + NoirTaggers.run_tagger(extected_endpoints, noir_options, "websocket") + extected_endpoints.each do |endpoint| + endpoint.tags.empty?.should be_false + endpoint.tags.each do |tag| + tag.name.should eq("websocket") + end + end + end end diff --git a/src/analyzer/analyzer.cr b/src/analyzer/analyzer.cr index d6b09e9f..1dbbac4f 100644 --- a/src/analyzer/analyzer.cr +++ b/src/analyzer/analyzer.cr @@ -10,6 +10,7 @@ def initialize_analyzers(logger : NoirLogger) analyzers["crystal_kemal"] = ->analyzer_crystal_kemal(Hash(Symbol, String)) analyzers["crystal_lucky"] = ->analyzer_crystal_lucky(Hash(Symbol, String)) analyzers["elixir_phoenix"] = ->analyzer_elixir_phoenix(Hash(Symbol, String)) + analyzers["go_beego"] = ->analyzer_go_beego(Hash(Symbol, String)) 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)) @@ -18,6 +19,7 @@ def initialize_analyzers(logger : NoirLogger) analyzers["java_jsp"] = ->analyzer_jsp(Hash(Symbol, String)) analyzers["java_spring"] = ->analyzer_java_spring(Hash(Symbol, String)) analyzers["js_express"] = ->analyzer_express(Hash(Symbol, String)) + analyzers["js_restify"] = ->analyzer_restify(Hash(Symbol, String)) analyzers["kotlin_spring"] = ->analyzer_kotlin_spring(Hash(Symbol, String)) analyzers["oas2"] = ->analyzer_oas2(Hash(Symbol, String)) analyzers["oas3"] = ->analyzer_oas3(Hash(Symbol, String)) @@ -30,6 +32,7 @@ def initialize_analyzers(logger : NoirLogger) analyzers["ruby_rails"] = ->analyzer_ruby_rails(Hash(Symbol, String)) analyzers["ruby_sinatra"] = ->analyzer_ruby_sinatra(Hash(Symbol, String)) analyzers["rust_axum"] = ->analyzer_rust_axum(Hash(Symbol, String)) + analyzers["rust_rocket"] = ->analyzer_rust_rocket(Hash(Symbol, String)) logger.info_sub "#{analyzers.size} Analyzers initialized" logger.debug "Analyzers:" diff --git a/src/analyzer/analyzers/analyzer_go_beego.cr b/src/analyzer/analyzers/analyzer_go_beego.cr new file mode 100644 index 00000000..fee940dd --- /dev/null +++ b/src/analyzer/analyzers/analyzer_go_beego.cr @@ -0,0 +1,150 @@ +require "../../models/analyzer" +require "../../minilexers/golang" + +class AnalyzerGoBeego < Analyzer + def analyze + # Source Analysis + public_dirs = [] of (Hash(String, String)) + groups = [] of Hash(String, String) + begin + Dir.glob("#{base_path}/**/*") do |path| + next if File.directory?(path) + if File.exists?(path) && File.extname(path) == ".go" + File.open(path, "r", encoding: "utf-8", invalid: :skip) do |file| + last_endpoint = Endpoint.new("", "") + file.each_line.with_index do |line, index| + details = Details.new(PathInfo.new(path, index + 1)) + lexer = GolangLexer.new + + if line.includes?(".Group(") + map = lexer.tokenize(line) + before = Token.new(:unknown, "", 0) + group_name = "" + group_path = "" + map.each do |token| + if token.type == :assign + group_name = before.value.to_s.gsub(":", "").gsub(/\s/, "") + end + + if token.type == :string + group_path = token.value.to_s + groups.each do |group| + group.each do |key, value| + if before.value.to_s.includes? key + group_path = value + group_path + end + end + end + end + + before = token + end + + if group_name.size > 0 && group_path.size > 0 + groups << { + group_name => group_path, + } + end + end + + if line.includes?(".Get(") || line.includes?(".Post(") || line.includes?(".Put(") || line.includes?(".Delete(") + get_route_path(line, groups).tap do |route_path| + if route_path.size > 0 + new_endpoint = Endpoint.new("#{route_path}", line.split(".")[1].split("(")[0].to_s.upcase, details) + result << new_endpoint + last_endpoint = new_endpoint + end + end + end + + if line.includes?(".Any(") || line.includes?(".Handler(") || line.includes?(".Router(") + get_route_path(line, groups).tap do |route_path| + if route_path.size > 0 + new_endpoint = Endpoint.new("#{route_path}", "GET", details) + result << new_endpoint + last_endpoint = new_endpoint + end + end + end + + ["GetString", "GetStrings", "GetInt", "GetInt8", "GetUint8", "GetInt16", "GetUint16", "GetInt32", "GetUint32", + "GetInt64", "GetUint64", "GetBool", "GetFloat"].each do |pattern| + match = line.match(/#{pattern}\(\"(.*)\"\)/) + if match + param_name = match[1] + last_endpoint.params << Param.new(param_name, "", "query") + end + end + + if line.includes?("GetCookie(") + match = line.match(/GetCookie\(\"(.*)\"\)/) + if match + cookie_name = match[1] + last_endpoint.params << Param.new(cookie_name, "", "cookie") + end + end + + if line.includes?("GetSecureCookie(") + match = line.match(/GetSecureCookie\(\"(.*)\"\)/) + if match + cookie_name = match[1] + last_endpoint.params << Param.new(cookie_name, "", "cookie") + end + end + end + end + end + end + rescue e + logger.debug e + end + + public_dirs.each do |p_dir| + full_path = (base_path + "/" + p_dir["file_path"]).gsub_repeatedly("//", "/") + Dir.glob("#{full_path}/**/*") do |path| + next if File.directory?(path) + if File.exists?(path) + if p_dir["static_path"].ends_with?("/") + p_dir["static_path"] = p_dir["static_path"][0..-2] + end + + details = Details.new(PathInfo.new(path)) + result << Endpoint.new("#{p_dir["static_path"]}#{path.gsub(full_path, "")}", "GET", details) + end + end + end + + Fiber.yield + + result + end + + def get_route_path(line : String, groups : Array(Hash(String, String))) : String + lexer = GolangLexer.new + map = lexer.tokenize(line) + before = Token.new(:unknown, "", 0) + map.each do |token| + if token.type == :string + final_path = token.value.to_s + groups.each do |group| + group.each do |key, value| + if before.value.to_s.includes? key + final_path = value + final_path + end + end + end + + return final_path + end + + before = token + end + + "" + end +end + +def analyzer_go_beego(options : Hash(Symbol, String)) + instance = AnalyzerGoBeego.new(options) + instance.analyze +end diff --git a/src/analyzer/analyzers/analyzer_java_spring.cr b/src/analyzer/analyzers/analyzer_java_spring.cr index c1e2d6df..8c0f611b 100644 --- a/src/analyzer/analyzers/analyzer_java_spring.cr +++ b/src/analyzer/analyzers/analyzer_java_spring.cr @@ -9,10 +9,27 @@ class AnalyzerJavaSpring < Analyzer def analyze parser_map = Hash(String, JavaParser).new package_map = Hash(String, Hash(String, ClassModel)).new + webflux_base_path_map = Hash(String, String).new Dir.glob("#{@base_path}/**/*") do |path| - next if File.directory?(path) + if File.directory?(path) + if path.ends_with?("/src") + config_path = File.join(path, "main/resources/application.yml") + if File.exists?(config_path) + config = YAML.parse(File.read(config_path)) rescue nil + if config && (spring = config["spring"]?) && (webflux = spring["webflux"]?) + webflux_base_path = webflux["base-path"]? + if webflux_base_path + webflux_base_path_map[path] = webflux_base_path.as_s + end + end + end + end + + next + end url = "" if File.exists?(path) && path.ends_with?(".java") + webflux_base_path = find_base_path(path, webflux_base_path_map) content = File.read(path, encoding: "utf-8", invalid: :skip) # Spring MVC Router (Controller) @@ -44,6 +61,7 @@ class AnalyzerJavaSpring < Analyzer next if path == _path if !parser_map.has_key?(_path) _parser = get_parser(Path.new(_path)) + parser_map[_path] = _parser else _parser = parser_map[_path] end @@ -115,7 +133,7 @@ class AnalyzerJavaSpring < Analyzer url_paths = Array(String).new # Spring MVC Decorator - request_method = nil + request_methods = Array(String).new if method_annotation.name.ends_with? "Mapping" parameter_format = nil annotation_parameters = method_annotation.params @@ -124,7 +142,20 @@ class AnalyzerJavaSpring < Analyzer annotation_parameter_key = annotation_parameter_tokens[0].value annotation_parameter_value = annotation_parameter_tokens[-1].value if annotation_parameter_key == "method" - request_method = annotation_parameter_value + if annotation_parameter_value == "}" + # multiple method + annotation_parameter_tokens.reverse_each do |token| + break if token.value == "method" + next if token.type == :LBRACE || token.type == :RBRACE + next if token.type == :DOT + known_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] + if known_methods.includes?(token.value) + request_methods.push(token.value) + end + end + else + request_methods.push(annotation_parameter_value) + end elsif annotation_parameter_key == "consumes" if annotation_parameter_value.ends_with? "APPLICATION_FORM_URLENCODED_VALUE" parameter_format = "form" @@ -135,6 +166,10 @@ class AnalyzerJavaSpring < Analyzer end end + if webflux_base_path.ends_with?("/") && url.starts_with?("/") + webflux_base_path = webflux_base_path[..-2] + end + if method_annotation.name == "RequestMapping" url_paths = [""] if method_annotation.params.size > 0 @@ -144,18 +179,20 @@ class AnalyzerJavaSpring < Analyzer line = method_annotation.tokens[0].line details = Details.new(PathInfo.new(path, line)) - if request_method.nil? + if request_methods.empty? # If the method is not annotated with @RequestMapping, then 5 methods are allowed ["GET", "POST", "PUT", "DELETE", "PATCH"].each do |_request_method| parameters = get_endpoint_parameters(parser, _request_method, method, parameter_format, class_map) url_paths.each do |url_path| - @result << Endpoint.new("#{url}#{url_path}", _request_method, parameters, details) + @result << Endpoint.new("#{webflux_base_path}#{url}#{url_path}", _request_method, parameters, details) end end else url_paths.each do |url_path| - parameters = get_endpoint_parameters(parser, request_method, method, parameter_format, class_map) - @result << Endpoint.new("#{url}#{url_path}", request_method, parameters, details) + request_methods.each do |request_method| + parameters = get_endpoint_parameters(parser, request_method, method, parameter_format, class_map) + @result << Endpoint.new("#{webflux_base_path}#{url}#{url_path}", request_method, parameters, details) + end end end break @@ -177,7 +214,7 @@ class AnalyzerJavaSpring < Analyzer details = Details.new(PathInfo.new(path, line)) url_paths.each do |url_path| - @result << Endpoint.new("#{url}#{url_path}", request_method, parameters, details) + @result << Endpoint.new("#{webflux_base_path}#{url}#{url_path}", request_method, parameters, details) end break end @@ -206,6 +243,16 @@ class AnalyzerJavaSpring < Analyzer @result end + def find_base_path(current_path : String, base_paths : Hash(String, String)) + base_paths.keys.sort_by!(&.size).reverse!.each do |path| + if current_path.starts_with?(path) + return base_paths[path] + end + end + + "" + end + def get_mapping_path(parser : JavaParser, tokens : Array(Token), method_params : Array(Array(Token))) # 1. Search for the value of the @xxxxxMapping annotation # 2. If the value is a string literal, return it diff --git a/src/analyzer/analyzers/analyzer_restify.cr b/src/analyzer/analyzers/analyzer_restify.cr new file mode 100644 index 00000000..9a165a1c --- /dev/null +++ b/src/analyzer/analyzers/analyzer_restify.cr @@ -0,0 +1,111 @@ +require "../../models/analyzer" + +class AnalyzerRestify < Analyzer + def analyze + # Source Analysis + begin + Dir.glob("#{base_path}/**/*") do |path| + next if File.directory?(path) + if File.exists?(path) + File.open(path, "r", encoding: "utf-8", invalid: :skip) do |file| + last_endpoint = Endpoint.new("", "") + file.each_line.with_index do |line, index| + endpoint = line_to_endpoint(line) + if endpoint.method != "" + details = Details.new(PathInfo.new(path, index + 1)) + endpoint.set_details(details) + result << endpoint + last_endpoint = endpoint + end + + param = line_to_param(line) + if param.name != "" + if last_endpoint.method != "" + last_endpoint.push_param(param) + end + end + end + end + end + end + rescue e + # TODO + end + + result + end + + def express_get_endpoint(line : String) + api_path = "" + splited = line.split("(") + if splited.size > 0 + api_path = splited[1].split(",")[0].gsub(/['"]/, "") + end + + api_path + end + + def line_to_param(line : String) : Param + if line.includes? "req.body." + param = line.split("req.body.")[1].split(")")[0].split("}")[0].split(";")[0] + return Param.new(param, "", "json") + end + + if line.includes? "req.query." + param = line.split("req.query.")[1].split(")")[0].split("}")[0].split(";")[0] + return Param.new(param, "", "query") + end + + if line.includes? "req.cookies." + param = line.split("req.cookies.")[1].split(")")[0].split("}")[0].split(";")[0] + return Param.new(param, "", "cookie") + end + + if line.includes? "req.header(" + param = line.split("req.header(")[1].split(")")[0].gsub(/['"]/, "") + return Param.new(param, "", "header") + end + + Param.new("", "", "") + end + + def line_to_endpoint(line : String) : Endpoint + if line.includes? ".get('/" + api_path = express_get_endpoint(line) + if api_path != "" + return Endpoint.new(api_path, "GET") + end + end + if line.includes? ".post('/" + api_path = express_get_endpoint(line) + if api_path != "" + return Endpoint.new(api_path, "POST") + end + end + if line.includes? ".put('/" + api_path = express_get_endpoint(line) + if api_path != "" + return Endpoint.new(api_path, "PUT") + end + end + if line.includes? ".delete('/" + api_path = express_get_endpoint(line) + if api_path != "" + return Endpoint.new(api_path, "DELETE") + end + end + if line.includes? ".patch('/" + api_path = express_get_endpoint(line) + if api_path != "" + return Endpoint.new(api_path, "PATCH") + end + end + + Endpoint.new("", "") + end +end + +def analyzer_restify(options : Hash(Symbol, String)) + instance = AnalyzerRestify.new(options) + instance.analyze +end diff --git a/src/analyzer/analyzers/analyzer_rust_rocket.cr b/src/analyzer/analyzers/analyzer_rust_rocket.cr new file mode 100644 index 00000000..dbf4abf0 --- /dev/null +++ b/src/analyzer/analyzers/analyzer_rust_rocket.cr @@ -0,0 +1,51 @@ +require "../../models/analyzer" + +class AnalyzerRustRocket < Analyzer + def analyze + # Source Analysis + pattern = /#\[(get|post|delete|put)\("([^"]+)"(?:, data = "<([^>]+)>")?\)\]/ + + begin + Dir.glob("#{base_path}/**/*") do |path| + next if File.directory?(path) + + if File.exists?(path) && File.extname(path) == ".rs" + File.open(path, "r", encoding: "utf-8", invalid: :skip) do |file| + file.each_line.with_index do |line, index| + if line.includes?("#[") && line.includes?(")]") + match = line.match(pattern) + if match + begin + callback_argument = match[1] + route_argument = match[2] + + details = Details.new(PathInfo.new(path, index + 1)) + result << Endpoint.new("#{route_argument}", callback_to_method(callback_argument), details) + rescue + end + end + end + end + end + end + end + rescue e + end + + result + end + + def callback_to_method(str) + method = str.split("(").first + if !["get", "post", "put", "delete"].includes?(method) + method = "get" + end + + method.upcase + end +end + +def analyzer_rust_rocket(options : Hash(Symbol, String)) + instance = AnalyzerRustRocket.new(options) + instance.analyze +end diff --git a/src/detector/detector.cr b/src/detector/detector.cr index 37bcf295..08f82a92 100644 --- a/src/detector/detector.cr +++ b/src/detector/detector.cr @@ -19,6 +19,7 @@ def detect_techs(base_path : String, options : Hash(Symbol, String), logger : No DetectorCrystalKemal, DetectorCrystalLucky, DetectorElixirPhoenix, + DetectorGoBeego, DetectorGoEcho, DetectorGoFiber, DetectorGoGin, @@ -27,6 +28,7 @@ def detect_techs(base_path : String, options : Hash(Symbol, String), logger : No DetectorJavaJsp, DetectorJavaSpring, DetectorJsExpress, + DetectorJsRestify, DetectorKotlinSpring, DetectorOas2, DetectorOas3, @@ -39,6 +41,7 @@ def detect_techs(base_path : String, options : Hash(Symbol, String), logger : No DetectorRubyRails, DetectorRubySinatra, DetectorRustAxum, + DetectorRustRocket, ]) channel = Channel(String).new diff --git a/src/detector/detectors/go_beego.cr b/src/detector/detectors/go_beego.cr new file mode 100644 index 00000000..9e74b57a --- /dev/null +++ b/src/detector/detectors/go_beego.cr @@ -0,0 +1,15 @@ +require "../../models/detector" + +class DetectorGoBeego < Detector + def detect(filename : String, file_contents : String) : Bool + if (filename.includes? "go.mod") && (file_contents.includes? "github.com/beego/beego") + true + else + false + end + end + + def set_name + @name = "go_beego" + end +end diff --git a/src/detector/detectors/js_restify.cr b/src/detector/detectors/js_restify.cr new file mode 100644 index 00000000..eea22a90 --- /dev/null +++ b/src/detector/detectors/js_restify.cr @@ -0,0 +1,21 @@ +require "../../models/detector" + +class DetectorJsRestify < Detector + def detect(filename : String, file_contents : String) : Bool + if (filename.includes? ".js") && (file_contents.includes? "require('restify')") + true + elsif (filename.includes? ".js") && (file_contents.includes? "require(\"restify\")") + true + elsif (filename.includes? ".ts") && (file_contents.includes? "server") + true + elsif (filename.includes? ".ts") && (file_contents.includes? "require(\"restify\")") + true + else + false + end + end + + def set_name + @name = "js_restify" + end +end diff --git a/src/detector/detectors/rust_rocket.cr b/src/detector/detectors/rust_rocket.cr new file mode 100644 index 00000000..e7bc77d2 --- /dev/null +++ b/src/detector/detectors/rust_rocket.cr @@ -0,0 +1,15 @@ +require "../../models/detector" + +class DetectorRustRocket < Detector + def detect(filename : String, file_contents : String) : Bool + check = file_contents.includes?("rocket") + check = check && file_contents.includes?("dependencies") + check = check && filename.includes?("Cargo.toml") + + check + end + + def set_name + @name = "rust_rocket" + end +end diff --git a/src/models/analyzer.cr b/src/models/analyzer.cr index 196a8e9b..94e6b351 100644 --- a/src/models/analyzer.cr +++ b/src/models/analyzer.cr @@ -1,5 +1,4 @@ require "./logger" -require "./code_block" require "./endpoint" class Analyzer diff --git a/src/models/code_block.cr b/src/models/code_block.cr deleted file mode 100644 index 187491a1..00000000 --- a/src/models/code_block.cr +++ /dev/null @@ -1,17 +0,0 @@ -struct CodeBlock - property depth : Int32 - property state : String - - def initialize - @depth = 1 - @state = "" - end - - def enter - @depth += 1 - end - - def exit - @depth -= 1 - end -end diff --git a/src/models/output_builder.cr b/src/models/output_builder.cr index 705009c1..2443e5a0 100644 --- a/src/models/output_builder.cr +++ b/src/models/output_builder.cr @@ -43,6 +43,12 @@ class OutputBuilder first_query = true first_form = true + if final_url.starts_with?("//") + if final_url[2] != ':' + final_url = final_url[1..] + end + end + if !params.nil? params.each do |param| if param.param_type == "query" diff --git a/src/noir.cr b/src/noir.cr index a3b6beb9..fcc5321d 100644 --- a/src/noir.cr +++ b/src/noir.cr @@ -6,7 +6,7 @@ require "./options.cr" require "./techs/techs.cr" module Noir - VERSION = "0.14.0" + VERSION = "0.15.0" end noir_options = default_options() diff --git a/src/tagger/tagger.cr b/src/tagger/tagger.cr index 26030ff0..c2174efe 100644 --- a/src/tagger/tagger.cr +++ b/src/tagger/tagger.cr @@ -13,6 +13,21 @@ module NoirTaggers desc: "Identifies OAuth endpoints", runner: OAuthTagger, }, + cors: { + name: "CORS Tagger", + desc: "Identifies CORS endpoints", + runner: CorsTagger, + }, + soap: { + name: "SOAP Tagger", + desc: "Identifies SOAP endpoints", + runner: SoapTagger, + }, + websocket: { + name: "Websocket Tagger", + desc: "Identifies Websocket endpoints", + runner: WebsocketTagger, + }, } def self.get_taggers diff --git a/src/tagger/taggers/cors.cr b/src/tagger/taggers/cors.cr new file mode 100644 index 00000000..673bd547 --- /dev/null +++ b/src/tagger/taggers/cors.cr @@ -0,0 +1,33 @@ +require "../../models/tagger" +require "../../models/endpoint" + +class CorsTagger < Tagger + WORDS = ["origin", "access-control-allow-origin", "access-control-request-method"] + + def initialize(options : Hash(Symbol, String)) + super + @name = "cors" + 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.downcase + 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 >= 1 + + if check + tag = Tag.new("cors", "CORS endpoint enabling cross-origin requests, allowing web applications from different domains to interact.", "CORS") + endpoint.add_tag(tag) + end + end + end +end diff --git a/src/tagger/taggers/soap.cr b/src/tagger/taggers/soap.cr new file mode 100644 index 00000000..9befab28 --- /dev/null +++ b/src/tagger/taggers/soap.cr @@ -0,0 +1,33 @@ +require "../../models/tagger" +require "../../models/endpoint" + +class SoapTagger < Tagger + WORDS = ["soapaction"] + + def initialize(options : Hash(Symbol, String)) + super + @name = "soap" + 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.downcase + 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 >= 1 + + if check + tag = Tag.new("soap", "SOAP endpoint for XML-based web service communication, supporting structured information exchanges across network applications.", "SOAP") + endpoint.add_tag(tag) + end + end + end +end diff --git a/src/tagger/taggers/websocket.cr b/src/tagger/taggers/websocket.cr new file mode 100644 index 00000000..a9b41868 --- /dev/null +++ b/src/tagger/taggers/websocket.cr @@ -0,0 +1,38 @@ +require "../../models/tagger" +require "../../models/endpoint" + +class WebsocketTagger < Tagger + WORDS = ["sec-websocket-key", "sec-websocket-accept", "sec-websocket-version"] + + def initialize(options : Hash(Symbol, String)) + super + @name = "websocket" + end + + def perform(endpoints : Array(Endpoint)) + endpoints.each do |endpoint| + tmp_params = [] of String + + if endpoint.protocol == "ws" + tag = Tag.new("websocket", "WebSocket endpoint for real-time, bidirectional communication between clients and servers, enabling efficient data exchanges.", "WebSocket") + endpoint.add_tag(tag) + else + endpoint.params.each do |param| + tmp_params.push param.name.to_s.downcase + 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 >= 2 + + if check + tag = Tag.new("websocket", "WebSocket endpoint for real-time, bidirectional communication between clients and servers, enabling efficient data exchanges.", "WebSocket") + endpoint.add_tag(tag) + end + end + end + end +end diff --git a/src/techs/techs.cr b/src/techs/techs.cr index ea543b85..c3793c09 100644 --- a/src/techs/techs.cr +++ b/src/techs/techs.cr @@ -4,117 +4,473 @@ module NoirTechs :framework => "Kemal", :language => "Crystal", :similar => ["kemal", "crystal-kemal", "crystal_kemal"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :crystal_lucky => { :framework => "Lucky", :language => "Crystal", :similar => ["lucky", "crystal-lucky", "crystal_lucky"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :cs_aspnet_mvc => { :framework => "ASP.NET MVC", :language => "C#", :similar => ["asp.net mvc", "cs-aspnet-mvc", "cs_aspnet_mvc", "c# asp.net mvc", "c#-asp.net-mvc", "c#_aspnet_mvc"], + :supported => { + :endpoint => true, + :method => false, + :params => { + :query => false, + :path => false, + :body => false, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, }, :elixir_phoenix => { :framework => "Phoenix", :language => "Elixir", :similar => ["phoenix", "elixir-phoenix", "elixir_phoenix"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => false, + :path => false, + :body => false, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => true, + }, + }, + :go_beego => { + :framework => "Beego", + :language => "Go", + :similar => ["beego", "go-beego", "go_beego"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => false, + :websocket => false, + }, }, :go_echo => { :framework => "Echo", :language => "Go", :similar => ["echo", "go-echo", "go_echo"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :go_fiber => { :framework => "Fiber", :language => "Go", :similar => ["fiber", "go-fiber", "go_fiber"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :go_gin => { :framework => "Gin", :language => "Go", :similar => ["gin", "go-gin", "go_gin"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + }, }, :har => { - :format => ["JSON"], - :similar => ["har"], + :format => ["JSON"], + :similar => ["har"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + }, }, :java_armeria => { :framework => "Armeria", :language => "Java", :similar => ["armeria", "java-armeria", "java_armeria"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => false, + :path => false, + :body => false, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, }, :java_jsp => { :framework => "JSP", :language => "Java", :similar => ["jsp", "java-jsp", "java_jsp"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => false, + :cookie => false, + }, + :static_path => true, + :websocket => false, + }, }, :java_spring => { :framework => "Spring", :language => "Java", :similar => ["spring", "java-spring", "java_spring"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, }, :js_express => { :framework => "Express", :language => "JavaScript", :similar => ["express", "js-express", "js_express"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => false, + :websocket => false, + }, + }, + :js_restify => { + :framework => "Restify", + :language => "JavaScript", + :similar => ["restify", "js-restify"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => false, + :websocket => false, + }, }, :kotlin_spring => { :framework => "Spring", :language => "Kotlin", :similar => ["spring", "kotlin-spring", "kotlin_spring"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => false, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, }, :oas2 => { - :format => ["JSON", "YAML"], - :similar => ["oas 2.0", "oas_2_0", "swagger 2.0", "swagger_2_0", "swagger"], + :format => ["JSON", "YAML"], + :similar => ["oas 2.0", "oas_2_0", "swagger 2.0", "swagger_2_0", "swagger"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + }, }, :oas3 => { - :format => ["JSON", "YAML"], - :similar => ["oas 3.0", "oas_3_0"], + :format => ["JSON", "YAML"], + :similar => ["oas 3.0", "oas_3_0"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + }, }, :php_pure => { :framework => "", :language => "PHP", :similar => ["php", "php-pure", "php_pure"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => false, + }, + :static_path => true, + :websocket => false, + }, }, :python_django => { :framework => "Django", :language => "Python", :similar => ["django", "python-django", "python_django"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :python_fastapi => { :framework => "FastAPI", :language => "Python", :similar => ["fastapi", "python-fastapi", "python_fastapi"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => false, + :websocket => false, + }, }, :python_flask => { :framework => "Flask", :language => "Python", :similar => ["flask", "python-flask", "python_flask"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => false, + :websocket => false, + }, }, :raml => { - :format => ["YAML"], - :similar => ["raml"], + :format => ["YAML"], + :similar => ["raml"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + }, }, :ruby_hanami => { :framework => "Hanami", :language => "Ruby", :similar => ["hanami", "ruby-hanami", "ruby_hanami"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => false, + :path => false, + :body => false, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, }, :ruby_rails => { :framework => "Rails", :language => "Ruby", :similar => ["rails", "ruby-rails", "ruby_rails"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :ruby_sinatra => { :framework => "Sinatra", :language => "Ruby", :similar => ["sinatra", "ruby-sinatra", "ruby_sinatra"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => true, + :websocket => false, + }, }, :rust_axum => { :framework => "Axum", :language => "Rust", :similar => ["axum", "rust-axum", "rust_axum"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => true, + :cookie => true, + }, + :static_path => false, + :websocket => false, + }, + }, + :rust_rocket => { + :framework => "Rocket", + :language => "Rust", + :similar => ["rocket", "rust-rocket", "rust_rocket"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => false, + :path => false, + :body => false, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, }, }