Compare commits

...
4 Commits
7 changed files with 64 additions and 2 deletions

No files matched your search

+2
View File
@@ -2,6 +2,8 @@ module github.com/BishopFox/joro
go 1.25.0
godebug multipathtcp=0
require github.com/hashicorp/go-uuid v1.0.3
require (
+15
View File
@@ -158,3 +158,18 @@ func (s *APIServer) handleClearRequests(w http.ResponseWriter, r *http.Request)
s.clearDetectFindingsWithHistory()
writeJSON(w, http.StatusOK, map[string]string{"status": "cleared"})
}
// handleDeleteRequestsByHost removes captured requests whose host matches a glob,
// the "clear what is already captured" companion to adding a noise pattern. The
// required "pattern" query param is the host glob (filepath.Match), the same value
// stored as the noise pattern. Removing the requests also drops them from the site
// map, which is derived from the same store.
func (s *APIServer) handleDeleteRequestsByHost(w http.ResponseWriter, r *http.Request) {
pattern := r.URL.Query().Get("pattern")
if pattern == "" {
writeError(w, http.StatusBadRequest, "pattern is required")
return
}
removed := s.store.DeleteByHostPattern(pattern)
writeJSON(w, http.StatusOK, map[string]int{"deleted": removed})
}
+1
View File
@@ -90,6 +90,7 @@ func registerRoutes(s *APIServer, mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/requests/{id}", s.handleGetRequest)
mux.HandleFunc("GET /api/v1/requests/{id}/params", s.handleGetRequestParams)
mux.HandleFunc("DELETE /api/v1/requests", s.handleClearRequests)
mux.HandleFunc("DELETE /api/v1/requests/by-host", s.handleDeleteRequestsByHost)
// Intercept
mux.HandleFunc("GET /api/v1/intercept", s.handleGetInterceptQueue)
+32
View File
@@ -3,6 +3,7 @@ package proxy
import (
"net/url"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
@@ -666,6 +667,37 @@ func (s *Store) DeleteSitemapNode(origin, path string, matchPath bool) int {
return removed
}
// DeleteByHostPattern removes every captured request whose host matches the given
// host glob, using the same filepath.Match predicate (case-insensitive) that the
// NoiseFilter applies in IsNoisy and Scope applies in matchRule. It is the "clear
// what is already captured" companion to adding a noise pattern, so the rows removed
// are exactly the ones the pattern suppresses going forward. Host is derived like
// Sitemap/DeleteSitemapNode (u.Host); requests whose URL cannot be parsed are kept.
// Returns the number of requests removed.
func (s *Store) DeleteByHostPattern(hostPattern string) int {
s.mu.Lock()
defer s.mu.Unlock()
pat := strings.ToLower(hostPattern)
kept := s.items[:0]
removed := 0
for _, r := range s.items {
u, err := url.Parse(r.URL)
if err != nil {
kept = append(kept, r)
continue
}
matched, merr := filepath.Match(pat, strings.ToLower(u.Host))
if merr != nil || !matched {
kept = append(kept, r)
continue
}
removed++
}
s.items = kept
return removed
}
// Clear removes all stored requests.
func (s *Store) Clear() {
s.mu.Lock()
+1 -1
View File
@@ -32,7 +32,7 @@ import (
"github.com/BishopFox/joro/internal/xsshunter"
)
var version = "v1.18.1"
var version = "v1.18.3"
var commit = "dev" // injected via -ldflags at build time
func main() {
+4
View File
@@ -1191,6 +1191,10 @@ export const api = {
if (path !== undefined) qs.set('path', path)
return req<{ deleted: number }>('DELETE', `/sitemap?${qs.toString()}`)
},
// Delete captured requests whose host matches a glob (the host-level companion to
// adding a noise pattern). pattern is the host glob stored as the noise pattern.
deleteRequestsByHost: (pattern: string) =>
req<{ deleted: number }>('DELETE', `/requests/by-host?${new URLSearchParams({ pattern }).toString()}`),
// Every parameter the node's requests carried. The node is 'origin', not 'host'
// as deleteSitemapNode names it: this endpoint also takes the tree's filters,
// where 'host' means a substring of the captured Host header. Filters must match
+9 -1
View File
@@ -1,6 +1,7 @@
import type { MenuItem } from '../components/ContextMenu'
import { api } from './api'
import { useToastStore } from '../stores/toastStore'
import { useRequestStore } from '../stores/requestStore'
// Host-level context-menu actions shared by the History table rows and the Site Map host
// menu. Modeled on getSelectionMenuItems. Scope and noise both match a host glob with
@@ -27,11 +28,18 @@ async function addScope(pattern: string, path: string, label: string) {
}
}
// addNoise adds a noise pattern and then purges the already-captured requests it matches:
// noise means traffic to never see, so leaving the old rows behind would be the surprise.
// invalidate() reloads History and, via the total change, the Site Map (both derive from the
// same store). The deletion predicate is the server's, matching the pattern just stored.
async function addNoise(pattern: string, label: string) {
try {
const { enabled } = await api.getNoise()
await api.addNoisePattern(pattern)
toast(enabled ? `Added ${label} to noise filter` : `Added ${label} to noise filter — noise filtering is off`)
const { deleted } = await api.deleteRequestsByHost(pattern)
if (deleted > 0) useRequestStore.getState().invalidate()
const cleared = deleted > 0 ? ` — cleared ${deleted} request${deleted === 1 ? '' : 's'}` : ''
toast(enabled ? `Added ${label} to noise filter${cleared}` : `Added ${label} to noise filter — noise filtering is off${cleared}`)
} catch {
toast('Failed to add noise pattern', 'error')
}