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 the ability to deny tags for flow filtering #2

Closed
wants to merge 1 commit into from
Closed
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
31 changes: 23 additions & 8 deletions webapp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,47 @@ async def api_flow_list(request):
services = request.query_params.getlist("service")
app_proto = request.query_params.get("app_proto")
search = request.query_params.get("search")
tags = request.query_params.getlist("tag")
required_tags = request.query_params.getlist("tag")
denied_tags = request.query_params.getlist("deny_tag")

if not ts_to.isnumeric():
raise HTTPException(400)

# Query flows and associated tags using filters
query = """
WITH fsrvs AS (SELECT value FROM json_each(?1)),
ftags AS (SELECT value FROM json_each(?2)),
fsearchfid AS (SELECT value FROM json_each(?5))
f_req_tags AS (SELECT value FROM json_each(?2)),
f_deny_tags AS (SELECT value FROM json_each(?3)),
fsearchfid AS (SELECT value FROM json_each(?6))
SELECT id, ts_start, ts_end, dest_ipport, app_proto,
(SELECT GROUP_CONCAT(tag) FROM alert WHERE flow_id = flow.id) AS tags
FROM flow WHERE ts_start <= ?3 AND (?4 IS NULL OR app_proto = ?4)
FROM flow WHERE ts_start <= ?4 AND (?5 IS NULL OR app_proto = ?5)
"""
if services == ["!"]:
# Filter flows related to no services
query += "AND NOT (src_ipport IN fsrvs OR dest_ipport IN fsrvs)"
services = sum(CTF_CONFIG["services"].values(), [])
elif services:
query += "AND (src_ipport IN fsrvs OR dest_ipport IN fsrvs)"
if tags:

if denied_tags:
# No alert with at least a denied tag exists for this flow
query += """
AND NOT EXISTS (
SELECT 1 FROM alert
WHERE flow_id == flow.id AND alert.tag IN f_deny_tags
)
"""

if required_tags:
# Relational division to get all flow_id matching all chosen tags
query += """
AND flow.id IN (
SELECT flow_id FROM alert WHERE tag IN ftags GROUP BY flow_id
HAVING COUNT(*) = (SELECT COUNT(*) FROM ftags)
SELECT flow_id FROM alert WHERE tag IN f_req_tags GROUP BY flow_id
HAVING COUNT(*) = (SELECT COUNT(*) FROM f_req_tags)
)
"""

search_fid = []
if search:
cursor = await payload_database.execute(
Expand All @@ -81,7 +95,8 @@ async def api_flow_list(request):
query,
(
json.dumps(services),
json.dumps(tags),
json.dumps(required_tags),
json.dumps(denied_tags),
int(ts_to) * 1000,
app_proto,
json.dumps(search_fid),
Expand Down
10 changes: 7 additions & 3 deletions webapp/static/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export default class Api {
* @param {Array} services Keep only flows matching these IP address and ports
* @param {String} appProto Keep only flows matching this app-layer protocol
* @param {String} search Search for this glob pattern in flows payloads
* @param {Array} tags Keep only flows matching these tags
* @param {Array} requiredTags Keep only flows matching these tags
* @param {Array} deniedTags Deny flows matching these tags
*/
async listFlows (timestampFrom, timestampTo, services, appProto, search, tags) {
async listFlows (timestampFrom, timestampTo, services, appProto, search, requiredTags, deniedTags) {
const url = new URL(`${location.origin}${location.pathname}api/flow`)
if (typeof timestampFrom === 'number') {
url.searchParams.append('from', timestampFrom)
Expand All @@ -39,9 +40,12 @@ export default class Api {
if (search) {
url.searchParams.append('search', search)
}
tags?.forEach((t) => {
requiredTags?.forEach((t) => {
url.searchParams.append('tag', t)
})
deniedTags?.forEach((t) => {
url.searchParams.append('deny_tag', t)
})
const response = await fetch(url.href, {})
if (!response.ok) {
throw Error('failed to list flows')
Expand Down
99 changes: 78 additions & 21 deletions webapp/static/js/flowlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,61 @@ class FlowList {
const tag = e.target.closest('a')?.dataset.tag
if (tag) {
const url = new URL(document.location)
const activeTags = url.searchParams.getAll('tag')
if (activeTags.includes(tag)) {
// Remove tag
url.searchParams.delete('tag')
activeTags.forEach(t => {
if (t !== tag) {
url.searchParams.append('tag', t)
}
})

let requiredTags = url.searchParams.getAll('tag')
let deniedTags = url.searchParams.getAll('deny_tag')

const is_required = requiredTags.includes(tag)
const is_denied = deniedTags.includes(tag)

let next_state;
if (is_required) {
if (e.shiftKey) {
// required => denied
next_state = 'denied'
} else {
// required => inactive
next_state = 'inactive'
}
} else if (is_denied) {
if (e.shiftKey) {
// denied => required
next_state = 'required'
} else {
// denied => inactive
next_state = 'inactive'
}
} else {
// Add tag
url.searchParams.append('tag', tag)
if (e.shiftKey) {
// inactive => denied
next_state = 'denied'
} else {
// inactive => required
next_state = 'required'
}
}

deniedTags = deniedTags.filter(t => t !== tag)
requiredTags = requiredTags.filter(t => t !== tag)

if (next_state == 'required') {
requiredTags.push(tag)
}
if (next_state == 'denied') {
deniedTags.push(tag)
}

url.searchParams.delete("tag")
url.searchParams.delete("deny_tag")

requiredTags.forEach(t => {
url.searchParams.append('tag', t)
})

deniedTags.forEach(t => {
url.searchParams.append('deny_tag', t)
})

window.history.pushState(null, '', url.href)
this.update()
e.preventDefault()
Expand Down Expand Up @@ -218,11 +260,20 @@ class FlowList {
* Build tag element
* @param {String} text Tag name
* @param {String} color Tag color
* @param {boolean} [fill=false] Active style
* @returns HTML element representing the tag
*/
tagBadge (text, color) {
tagBadge (text, color, fill = true) {
const badge = document.createElement('span')
badge.classList.add('badge', `text-bg-${color ?? 'none'}`, 'mb-1', 'me-1', 'p-1')
badge.classList.add('badge', 'mb-1', 'me-1', 'p-1')
if (fill) {
badge.classList.add(`text-bg-${color ?? 'none'}`)
} else {
badge.classList.add(
`border-${color ?? 'none'}`,
`text-${color ?? 'none'}`
)
}
badge.textContent = text
return badge
}
Expand Down Expand Up @@ -271,11 +322,15 @@ class FlowList {
// Create tag and append to dropdown
const { tag, color } = t
const url = new URL(document.location)
const activeTags = url.searchParams.getAll('tag')
const badge = this.tagBadge(tag, color)
badge.classList.add('border', 'border-2')
badge.classList.toggle('border-purple', activeTags.includes(tag))
badge.classList.toggle('text-bg-purple', activeTags.includes(tag))

const is_required = url.searchParams.getAll('tag').includes(tag)
const is_denied = url.searchParams.getAll('deny_tag').includes(tag)

const is_active = is_required || is_denied

const badge = this.tagBadge(tag, color, is_active)
badge.classList.toggle('text-decoration-line-through', is_denied)

const link = document.createElement('a')
link.href = '#'
link.dataset.tag = tag
Expand Down Expand Up @@ -381,14 +436,16 @@ class FlowList {
const services = url.searchParams.getAll('service')
const filterAppProto = url.searchParams.get('app_proto')
const filterSearch = url.searchParams.get('search')
const filterTags = url.searchParams.getAll('tag')
const filterRequiredTags = url.searchParams.getAll('tag')
const filterDeniedTags = url.searchParams.getAll('deny_tag')
const { flows, appProto, tags } = await this.apiClient.listFlows(
fromTs ? Number(fromTs) : null,
toTs ? Number(toTs) : null,
services,
filterAppProto,
filterSearch,
filterTags
filterRequiredTags,
filterDeniedTags
)

// Update search input
Expand All @@ -402,7 +459,7 @@ class FlowList {
this.updateActiveFlow()

// Update filter dropdown visual indicator
document.querySelector('#dropdown-filter > button').classList.toggle('text-bg-purple', toTs || filterTags.length || filterAppProto || filterSearch)
document.querySelector('#dropdown-filter > button').classList.toggle('text-bg-purple', toTs || filterRequiredTags.length || filterDeniedTags.length || filterAppProto || filterSearch)

// Update service filter select state
document.getElementById('services-select').value = services.join(',')
Expand Down
Loading