mirror of
https://github.com/Av0idD4rk/packmate.git
synced 2026-10-23 15:31:00 +00:00
Add packet IP address display and search
This commit is contained in:
parent
8f23d97100
commit
9896d4b104
@ -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",
|
||||
|
||||
@ -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>
|
||||
→
|
||||
<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);
|
||||
|
||||
@ -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}'`;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
79
tests/ip-patterns.test.cjs
Normal file
79
tests/ip-patterns.test.cjs
Normal 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');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user