Add packet IP address display and search

This commit is contained in:
av0idd4rk 2026-10-10 03:40:43 +03:00
parent 8f23d97100
commit 9896d4b104
6 changed files with 129 additions and 4 deletions

View File

@ -5,7 +5,8 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"lint": "vue-cli-service lint",
"test:ip-patterns": "node --test tests/ip-patterns.test.cjs"
},
"dependencies": {
"axios": "^1.4.0",

View File

@ -7,6 +7,11 @@
<button @click.prevent="copyText" class="btn btn-link">Copy text</button>
<button @click.prevent="copyPythonBytes" class="btn btn-link">Copy as Python bytes</button>
</div>
<div v-if="packet.sourceIp || packet.destinationIp" class="packet-addresses">
<span title="Source IP" :style="ipHighlightStyle('SOURCE_IP')">{{ packet.sourceIp || '?' }}</span>
&rarr;
<span title="Destination IP" :style="ipHighlightStyle('DESTINATION_IP')">{{ packet.destinationIp || '?' }}</span>
</div>
<p v-if="!this.$store.state.hexdumpMode"
class="pt-2 pb-2 mb-3"
v-html="stringdata"/>
@ -25,6 +30,8 @@
id: Number(),
matches: Array(),
timestamp: Number(),
sourceIp: String(),
destinationIp: String(),
incoming: Boolean(),
ungzipped: Boolean(),
webSocketParsed: Boolean(),
@ -50,6 +57,13 @@
},
},
methods: {
ipHighlightStyle(target) {
const match = (this.packet.matches || []).find(match => match.searchTarget === target &&
this.$store.state.patterns.some(pattern => pattern.id === match.patternId && !pattern.deleted));
if (!match) return {};
const pattern = this.$store.state.patterns.find(pattern => pattern.id === match.patternId);
return {backgroundColor: pattern.color,};
},
atou(b64) {
const text = atob(b64);
const length = text.length;
@ -127,6 +141,7 @@
}, {}); // Array to object
let offset = 0;
this.packet.matches
.filter(match => !match.searchTarget || match.searchTarget === 'CONTENT')
.filter(match => !patterns[match.patternId].deleted)
.sort((a, b) => a.startPosition - b.startPosition)
.forEach(match => {
@ -179,6 +194,11 @@
</script>
<style scoped>
.packet-addresses {
font-family: "Ubuntu Mono", "Lucida Console", monospace;
overflow-wrap: anywhere;
}
.packet-outgoing {
background: var(--response-packet-color);
box-shadow: 0 0 5px 5px var(--response-packet-color);

View File

@ -18,6 +18,7 @@
<s v-else :style="`color: ${pattern.color};`">{{ pattern.name }}</s>:
<code>{{ getSearchTypeValue(pattern.searchType, pattern.value) }}</code>;
<span v-if="pattern.searchTarget && pattern.searchTarget !== 'CONTENT'">{{ getSearchTargetLabel(pattern.searchTarget) }}; </span>
<template v-if="pattern.actionType === 'FIND'">search </template>
<template v-else>ignore </template>
@ -89,6 +90,9 @@
},
},
methods: {
getSearchTargetLabel(target) {
return {SOURCE_IP: 'source IP', DESTINATION_IP: 'destination IP', EITHER_IP: 'either IP address',}[target];
},
getSearchTypeValue(searchType, value) {
if (searchType === 'REGEX') return `/${value}/`;
else if (searchType === 'SUBSTRING') return `'${value}'`;

View File

@ -203,7 +203,7 @@ export default {
const currentPort = parseInt(this.$route?.params?.servicePort, 10);
if (currentPort && currentPort !== stream.service) return;
if (this.$store.state.displayFavoritesOnly || this.$store.state.pause) return;
if (this.$route.query.pattern && !stream.foundPatterns.some(e => e.id === this.$route.query.pattern)) {
if (this.$route.query.pattern && !(stream.foundPatternsIds || []).includes(Number(this.$route.query.pattern))) {
return;
}

View File

@ -8,7 +8,7 @@
label-cols-sm="4"
label-cols-lg="3"
label="Name"
description="It is displayed in the list and highlighted in the packet contents"
description="It is displayed in the list and highlights matching content or IP addresses"
label-for="pattern-name">
<b-form-input @keydown.native.enter="addPattern" id="pattern-name" required v-model="newPattern.name"/>
</b-form-group>
@ -47,6 +47,21 @@
<b-form-input id="pattern-color" required type="color" v-model="newPattern.color"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search in"
description="IP addresses use regular expressions. Use ^ and $ to match the entire address."
label-for="pattern-searchTarget">
<b-form-select id="pattern-searchTarget" required v-model="newPattern.searchTarget" @change="onSearchTargetChange">
<option value="CONTENT">Packet content</option>
<option value="SOURCE_IP">Source IP</option>
<option value="DESTINATION_IP">Destination IP</option>
<option value="EITHER_IP">Either IP address</option>
</b-form-select>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
@ -54,7 +69,7 @@
label="Search method"
description="The way to search for patterns in packets"
label-for="pattern-searchType">
<b-form-select id="pattern-searchType" required v-model="newPattern.searchType">
<b-form-select id="pattern-searchType" required v-model="newPattern.searchType" :disabled="newPattern.searchTarget !== 'CONTENT'">
<option value="REGEX" selected>Regular expression</option>
<option value="SUBSTRING">Substring</option>
<option value="SUBBYTES">Bytes</option>
@ -97,6 +112,7 @@ const defaultPattern = {
value: '',
color: '#FF7474',
searchType: 'SUBSTRING',
searchTarget: 'CONTENT',
directionType: 'BOTH',
actionType: 'FIND',
serviceId: null,
@ -112,6 +128,7 @@ const defaultPattern = {
value: String,
color: String,
searchType: String,
searchTarget: String,
directionType: String,
actionType: String,
serviceId: Number,
@ -145,7 +162,11 @@ const defaultPattern = {
},
},
methods: {
onSearchTargetChange(target) {
if (target !== 'CONTENT') this.newPattern.searchType = 'REGEX';
},
getPlaceholder() {
if (this.newPattern.searchTarget && this.newPattern.searchTarget !== 'CONTENT') return '^10\\.20\\.30\\.[0-9]+$';
if (this.newPattern.searchType === 'REGEX') return '[A-Z0-9]{31}=';
else if (this.newPattern.searchType === 'SUBSTRING') return 'HTTP/2';
else return 'DEAD BEEF 1337';

View File

@ -0,0 +1,79 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const test = require('node:test');
const compiler = require('vue-template-compiler');
// Exercise the actual component methods without a browser or an extra test framework.
function component(relativePath) {
const file = fs.readFileSync(path.join(__dirname, '../src', relativePath), 'utf8');
const source = compiler.parseComponent(file).script.content
.replace(/^\s*import .*$/gm, '')
.replace('export default', 'module.exports =');
const sandbox = {module: {exports: {}}, SidebarStream: {}, PatternModal: {}, LookBack: {}, console};
vm.runInNewContext(source, sandbox);
return sandbox.module.exports;
}
test('IP selection uses regex and provides an escaped IPv4 example', () => {
const methods = component('views/PatternModal.vue').methods;
const context = {newPattern: {searchTarget: 'SOURCE_IP', searchType: 'SUBBYTES'}};
methods.onSearchTargetChange.call(context, 'SOURCE_IP');
assert.equal(context.newPattern.searchType, 'REGEX');
assert.equal(methods.getPlaceholder.call(context), '^10\\.20\\.30\\.[0-9]+$');
});
test('live filter accepts matching DTO IDs and rejects other streams', () => {
const addStream = component('components/Sidebar.vue').methods.addStreamFromWs;
const context = {
$route: {params: {servicePort: '80'}, query: {pattern: '7'}},
$store: {state: {displayFavoritesOnly: false, pause: false}},
streams: [],
};
addStream.call(context, {id: 1, service: 80, foundPatternsIds: [7]});
addStream.call(context, {id: 2, service: 80, foundPatternsIds: [8]});
addStream.call(context, {id: 3, service: 443, foundPatternsIds: [7]});
addStream.call(context, {id: 4, service: 80});
assert.deepEqual(context.streams.map(stream => stream.id), [1]);
});
test('IP matches highlight endpoints without changing payload highlights', () => {
const methods = component('components/Packet.vue').methods;
const context = {
$store: {state: {patterns: [
{id: 1, color: '#112233', deleted: false},
{id: 2, color: '#445566', deleted: false},
]}},
packet: {matches: [
{patternId: 1, searchTarget: 'SOURCE_IP', startPosition: 0, endPosition: 10},
{patternId: 2, searchTarget: 'CONTENT', startPosition: 0, endPosition: 3},
]},
};
assert.equal(methods.ipHighlightStyle.call(context, 'SOURCE_IP').backgroundColor, '#112233');
assert.equal(Object.keys(methods.ipHighlightStyle.call(context, 'DESTINATION_IP')).length, 0);
assert.equal(methods.highlightPatterns.call(context, 'FLAG rest'),
'<span style="background-color: #445566">FLAG</span> rest');
context.packet.matches = [context.packet.matches[0]];
assert.equal(methods.highlightPatterns.call(context, 'unchanged'), 'unchanged');
context.$store.state.patterns[0].deleted = true;
assert.equal(Object.keys(methods.ipHighlightStyle.call(context, 'SOURCE_IP')).length, 0);
});
test('legacy content matches without a target retain their highlights', () => {
const methods = component('components/Packet.vue').methods;
const context = {
$store: {state: {patterns: [{id: 1, color: '#112233', deleted: false}]}},
packet: {matches: [{patternId: 1, startPosition: 0, endPosition: 3}]},
};
assert.equal(methods.highlightPatterns.call(context, 'FLAG'),
'<span style="background-color: #112233">FLAG</span>');
});
test('pattern list identifies the selected IP field', () => {
const method = component('components/PatternsDropdown.vue').methods.getSearchTargetLabel;
assert.equal(method('SOURCE_IP'), 'source IP');
assert.equal(method('DESTINATION_IP'), 'destination IP');
assert.equal(method('EITHER_IP'), 'either IP address');
});