mirror of
https://github.com/Av0idD4rk/packmate.git
synced 2026-10-24 22:09:25 +00:00
Add packet IP address display and search
This commit is contained in:
parent
8f23d97100
commit
9896d4b104
@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"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": {
|
"dependencies": {
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
|||||||
@ -7,6 +7,11 @@
|
|||||||
<button @click.prevent="copyText" class="btn btn-link">Copy text</button>
|
<button @click.prevent="copyText" class="btn btn-link">Copy text</button>
|
||||||
<button @click.prevent="copyPythonBytes" class="btn btn-link">Copy as Python bytes</button>
|
<button @click.prevent="copyPythonBytes" class="btn btn-link">Copy as Python bytes</button>
|
||||||
</div>
|
</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"
|
<p v-if="!this.$store.state.hexdumpMode"
|
||||||
class="pt-2 pb-2 mb-3"
|
class="pt-2 pb-2 mb-3"
|
||||||
v-html="stringdata"/>
|
v-html="stringdata"/>
|
||||||
@ -25,6 +30,8 @@
|
|||||||
id: Number(),
|
id: Number(),
|
||||||
matches: Array(),
|
matches: Array(),
|
||||||
timestamp: Number(),
|
timestamp: Number(),
|
||||||
|
sourceIp: String(),
|
||||||
|
destinationIp: String(),
|
||||||
incoming: Boolean(),
|
incoming: Boolean(),
|
||||||
ungzipped: Boolean(),
|
ungzipped: Boolean(),
|
||||||
webSocketParsed: Boolean(),
|
webSocketParsed: Boolean(),
|
||||||
@ -50,6 +57,13 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
atou(b64) {
|
||||||
const text = atob(b64);
|
const text = atob(b64);
|
||||||
const length = text.length;
|
const length = text.length;
|
||||||
@ -127,6 +141,7 @@
|
|||||||
}, {}); // Array to object
|
}, {}); // Array to object
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
this.packet.matches
|
this.packet.matches
|
||||||
|
.filter(match => !match.searchTarget || match.searchTarget === 'CONTENT')
|
||||||
.filter(match => !patterns[match.patternId].deleted)
|
.filter(match => !patterns[match.patternId].deleted)
|
||||||
.sort((a, b) => a.startPosition - b.startPosition)
|
.sort((a, b) => a.startPosition - b.startPosition)
|
||||||
.forEach(match => {
|
.forEach(match => {
|
||||||
@ -179,6 +194,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.packet-addresses {
|
||||||
|
font-family: "Ubuntu Mono", "Lucida Console", monospace;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.packet-outgoing {
|
.packet-outgoing {
|
||||||
background: var(--response-packet-color);
|
background: var(--response-packet-color);
|
||||||
box-shadow: 0 0 5px 5px 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>:
|
<s v-else :style="`color: ${pattern.color};`">{{ pattern.name }}</s>:
|
||||||
|
|
||||||
<code>{{ getSearchTypeValue(pattern.searchType, pattern.value) }}</code>;
|
<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-if="pattern.actionType === 'FIND'">search </template>
|
||||||
<template v-else>ignore </template>
|
<template v-else>ignore </template>
|
||||||
@ -89,6 +90,9 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getSearchTargetLabel(target) {
|
||||||
|
return {SOURCE_IP: 'source IP', DESTINATION_IP: 'destination IP', EITHER_IP: 'either IP address',}[target];
|
||||||
|
},
|
||||||
getSearchTypeValue(searchType, value) {
|
getSearchTypeValue(searchType, value) {
|
||||||
if (searchType === 'REGEX') return `/${value}/`;
|
if (searchType === 'REGEX') return `/${value}/`;
|
||||||
else if (searchType === 'SUBSTRING') return `'${value}'`;
|
else if (searchType === 'SUBSTRING') return `'${value}'`;
|
||||||
|
|||||||
@ -203,7 +203,7 @@ export default {
|
|||||||
const currentPort = parseInt(this.$route?.params?.servicePort, 10);
|
const currentPort = parseInt(this.$route?.params?.servicePort, 10);
|
||||||
if (currentPort && currentPort !== stream.service) return;
|
if (currentPort && currentPort !== stream.service) return;
|
||||||
if (this.$store.state.displayFavoritesOnly || this.$store.state.pause) 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
label-cols-sm="4"
|
label-cols-sm="4"
|
||||||
label-cols-lg="3"
|
label-cols-lg="3"
|
||||||
label="Name"
|
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">
|
label-for="pattern-name">
|
||||||
<b-form-input @keydown.native.enter="addPattern" id="pattern-name" required v-model="newPattern.name"/>
|
<b-form-input @keydown.native.enter="addPattern" id="pattern-name" required v-model="newPattern.name"/>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
@ -47,6 +47,21 @@
|
|||||||
<b-form-input id="pattern-color" required type="color" v-model="newPattern.color"/>
|
<b-form-input id="pattern-color" required type="color" v-model="newPattern.color"/>
|
||||||
</b-form-group>
|
</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
|
<b-form-group
|
||||||
v-if="creating"
|
v-if="creating"
|
||||||
label-cols-sm="4"
|
label-cols-sm="4"
|
||||||
@ -54,7 +69,7 @@
|
|||||||
label="Search method"
|
label="Search method"
|
||||||
description="The way to search for patterns in packets"
|
description="The way to search for patterns in packets"
|
||||||
label-for="pattern-searchType">
|
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="REGEX" selected>Regular expression</option>
|
||||||
<option value="SUBSTRING">Substring</option>
|
<option value="SUBSTRING">Substring</option>
|
||||||
<option value="SUBBYTES">Bytes</option>
|
<option value="SUBBYTES">Bytes</option>
|
||||||
@ -97,6 +112,7 @@ const defaultPattern = {
|
|||||||
value: '',
|
value: '',
|
||||||
color: '#FF7474',
|
color: '#FF7474',
|
||||||
searchType: 'SUBSTRING',
|
searchType: 'SUBSTRING',
|
||||||
|
searchTarget: 'CONTENT',
|
||||||
directionType: 'BOTH',
|
directionType: 'BOTH',
|
||||||
actionType: 'FIND',
|
actionType: 'FIND',
|
||||||
serviceId: null,
|
serviceId: null,
|
||||||
@ -112,6 +128,7 @@ const defaultPattern = {
|
|||||||
value: String,
|
value: String,
|
||||||
color: String,
|
color: String,
|
||||||
searchType: String,
|
searchType: String,
|
||||||
|
searchTarget: String,
|
||||||
directionType: String,
|
directionType: String,
|
||||||
actionType: String,
|
actionType: String,
|
||||||
serviceId: Number,
|
serviceId: Number,
|
||||||
@ -145,7 +162,11 @@ const defaultPattern = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onSearchTargetChange(target) {
|
||||||
|
if (target !== 'CONTENT') this.newPattern.searchType = 'REGEX';
|
||||||
|
},
|
||||||
getPlaceholder() {
|
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}=';
|
if (this.newPattern.searchType === 'REGEX') return '[A-Z0-9]{31}=';
|
||||||
else if (this.newPattern.searchType === 'SUBSTRING') return 'HTTP/2';
|
else if (this.newPattern.searchType === 'SUBSTRING') return 'HTTP/2';
|
||||||
else return 'DEAD BEEF 1337';
|
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