Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add go-chi detector & analyzer #529

Merged
merged 7 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions spec/functional_test/fixtures/go/chi/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/hahwul/test-go-app

go 1.20

require github.com/go-chi/chi/v5 v5.2.0
94 changes: 94 additions & 0 deletions spec/functional_test/fixtures/go/chi/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import (
//...
"context"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)

func main() {
r := chi.NewRouter()

// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

// Set a timeout value on the request context (ctx), that will signal
// through ctx.Done() that the request has timed out and further
// processing should be stopped.
r.Use(middleware.Timeout(60 * time.Second))

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})

// RESTy routes for "articles" resource
r.Route("/articles", func(r chi.Router) {
r.With(paginate).Get("/", listArticles) // GET /articles
r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017

r.Post("/", createArticle) // POST /articles
r.Get("/search", searchArticles) // GET /articles/search

// Regexp url parameters:
r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto

// Subrouters:
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx)
r.Get("/", getArticle) // GET /articles/123
r.Put("/", updateArticle) // PUT /articles/123
r.Delete("/", deleteArticle) // DELETE /articles/123
})
})

// Mount the admin sub-router
r.Mount("/admin", adminRouter())

http.ListenAndServe(":3333", r)
}

func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "articleID")
article, err := dbGetArticle(articleID)
if err != nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), "article", article)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func getArticle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
article, ok := ctx.Value("article").(*Article)
if !ok {
http.Error(w, http.StatusText(422), 422)
return
}
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
}

// A completely separate router for administrator routes
func adminRouter() http.Handler {
r := chi.NewRouter()
r.Use(AdminOnly)
r.Get("/", adminIndex)
r.Get("/accounts", adminListAccounts)
return r
}

func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
if !ok || !perm.IsAdmin() {
http.Error(w, http.StatusText(403), 403)
return
}
next.ServeHTTP(w, r)
})
}
19 changes: 19 additions & 0 deletions spec/functional_test/testers/go/chi_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "../../func_spec.cr"

expected_endpoints = [
Endpoint.new("/", "GET"),
Endpoint.new("/articles/", "POST"),
Endpoint.new("/articles/search", "GET"),
Endpoint.new("/articles/{articleSlug:[a-z-]+}", "GET", [Param.new("articleSlug", "", "path")]),
Endpoint.new("/articles/{articleID}/", "GET", [Param.new("articleID", "", "path")]),
Endpoint.new("/articles/{articleID}/", "PUT", [Param.new("articleID", "", "path")]),
Endpoint.new("/articles/{articleID}/", "DELETE", [Param.new("articleID", "", "path")]),
Endpoint.new("/admin/", "GET"),
Endpoint.new("/admin/accounts", "GET"),
Endpoint.new("/accounts", "GET"),
]

FunctionalTester.new("fixtures/go/chi/", {
:techs => 1,
:endpoints => expected_endpoints.size,
}, expected_endpoints).test_all
11 changes: 11 additions & 0 deletions spec/unit_test/detector/go/chi_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "../../../../src/detector/detectors/go/*"

describe "Detect Go Chi" do
config_init = ConfigInitializer.new
options = config_init.default_options
instance = Detector::Go::Chi.new options

it "go.mod" do
instance.detect("go.mod", "github.com/go-chi/chi").should eq(true)
end
end
1 change: 1 addition & 0 deletions src/analyzer/analyzer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def initialize_analyzers(logger : NoirLogger)
{"go_echo", Go::Echo},
{"go_fiber", Go::Fiber},
{"go_gin", Go::Gin},
{"go_chi", Go::Chi},
{"har", Specification::Har},
{"java_armeria", Java::Armeria},
{"java_jsp", Java::Jsp},
Expand Down
137 changes: 137 additions & 0 deletions src/analyzer/analyzers/go/chi.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
require "../../../models/analyzer"
require "../../../minilexers/golang"

module Analyzer::Go
class Chi < Analyzer
def analyze
result = [] of Endpoint
channel = Channel(String).new
begin
spawn do
Dir.glob("#{@base_path}/**/*") do |file|
channel.send(file)
end
channel.close
end

WaitGroup.wait do |wg|
@options["concurrency"].to_s.to_i.times do
wg.spawn do
loop do
path = channel.receive?
break if path.nil?
next if File.directory?(path)
if File.exists?(path) && File.extname(path) == ".go"
File.open(path, "r", encoding: "utf-8", invalid: :skip) do |file|
prefix_stack = [] of String
file.each_line.with_index do |line, index|
details = Details.new(PathInfo.new(path, index + 1))

# Mount 처리: 객체가 무엇이든 Mount 호출 인식
if line.includes?(".Mount(")
if match = line.match(/[a-zA-Z]\w*\.Mount\(\s*"([^"]+)"\s*,\s*([^(]+)\(\)/)
mount_prefix = match[1]
router_function = match[2]
endpoints = analyze_router_function(path, router_function)
endpoints.each do |ep|
ep.url = mount_prefix + ep.url
result << ep
end
end
next
end

# Route 블록 처리: 객체가 무엇이든 Route 호출 인식 (접두사 저장)
if line.includes?(".Route(")
if match = line.match(/[a-zA-Z]\w*\.Route\(\s*"([^"]+)"/)
prefix_stack << match[1]
end
next
end

# 헤더 호출 건너뛰기 (예: c.Header.Get("X-API-Key"))
if line.includes?(".Header.Get(")
# TODO: 생각보다 복잡해져서 나중에 구현하기
next
end

# 블록 종료 시 접두사 제거 (단순히 '}'만 있는 줄로 처리)
if line.strip == "}" && !prefix_stack.empty?
prefix_stack.pop
next
end

# 실제 endpoint 처리: .Get, .Post, .Put, .Delete (임의 객체)
method = ""
route_path = ""
if match = line.match(/[a-zA-Z]\w*\.(Get|Post|Put|Delete)\(\s*"([^"]+)"/)
method = match[1].upcase
route_path = match[2]
end

if method.size > 0 && route_path.size > 0
full_route = prefix_stack.join("") + route_path
result << Endpoint.new(full_route, method, details)
end
end
end
end
rescue e : File::NotFoundError
logger.debug "File not found: #{path}"
end
end
end
end
rescue e
logger.debug e
end
result
end

# 추출: 객체.메서드("경로", ...) 형태에서 경로 추출
def extract_route_path(line : String) : String
if match = line.match(/[a-zA-Z]\w*\.\w+\(\s*"([^"]+)"/)
return match[1]
end
""
end

# 주어진 파일 내에 정의된 router 함수의 내용을 추출하여 엔드포인트 정보로 변환
def analyze_router_function(file_path : String, func_name : String) : Array(Endpoint)
endpoints = [] of Endpoint
content = File.read(file_path)
if content.includes?("func #{func_name}(")
block_started = false
brace_count = 0
content.each_line do |line|
if !block_started
if line.includes?("func #{func_name}(")
block_started = true
if line.includes?("{")
brace_count += 1
end
end
next
else
brace_count += line.count("{")
brace_count -= line.count("}")
details = Details.new(PathInfo.new(file_path))
method = ""
route_path = ""
if match = line.match(/[a-zA-Z]\w*\.(Get|Post|Put|Delete)\(\s*"([^"]+)"/)
method = match[1].upcase
route_path = match[2]
end

if method.size > 0 && route_path.size > 0
endpoints << Endpoint.new(route_path, method, details)
end

break if brace_count <= 0
end
end
end
endpoints
end
end
end
1 change: 1 addition & 0 deletions src/detector/detector.cr
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def detect_techs(base_path : String, options : Hash(String, YAML::Any), passive_
Go::Echo,
Go::Fiber,
Go::Gin,
Go::Chi,
Specification::Har,
Java::Armeria,
Java::Jsp,
Expand Down
17 changes: 17 additions & 0 deletions src/detector/detectors/go/chi.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "../../../models/detector"

module Detector::Go
class Chi < Detector
def detect(filename : String, file_contents : String) : Bool
if (filename.includes? "go.mod") && (file_contents.includes? "github.com/go-chi/chi")
true
else
false
end
end

def set_name
@name = "go_chi"
end
end
end
18 changes: 18 additions & 0 deletions src/techs/techs.cr
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,24 @@ module NoirTechs
},
},
},
:go_chi => {
:framework => "Chi",
:language => "Go",
:similar => ["chi", "go-chi", "go_chi"],
:supported => {
:endpoint => true,
:method => true,
:params => {
:query => true,
:path => true,
:body => true,
:header => false,
:cookie => false,
},
:static_path => false,
:websocket => false,
},
},
:har => {
:format => ["JSON"],
:similar => ["har"],
Expand Down