Skip to content

Commit 66636ec

Browse files
author
Qyn
committed
Fixes #11 #9 #5 #3 and a few other bugs
1 parent fd63d22 commit 66636ec

File tree

14 files changed

+184
-88
lines changed

14 files changed

+184
-88
lines changed

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ services:
6565
networks:
6666
- internal
6767
volumes:
68-
- /traffic:/traffic:ro
68+
- /home/qyn/tulip/traffic:/traffic:ro
6969
command: "./enricher -eve /traffic/eve.json"
7070
environment:
7171
TULIP_MONGO: mongo:27017

frontend/src/api.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export interface Flow {
1414
// TODO: get this from backend instead of hacky work around
1515
service_tag: string
1616
inx: number
17-
starred: boolean
18-
blocked: boolean
17+
parent_id: Id
18+
child_id: Id
1919
tags: string[]
2020
suricata: number[]
2121
filename: string
@@ -76,12 +76,10 @@ class TulipApi {
7676
async _getFlows(query: FlowsQuery, destToService: any) {
7777

7878
// HACK: make starred look like a tag
79-
const starred = query.tags.includes("starred");
80-
let tags = query.tags.filter(tag => tag !== "starred")
79+
let tags = query.tags;
8180
const hacky_query = {
8281
...query,
8382
tags: tags.length > 0 ? tags : undefined,
84-
starred
8583
}
8684
// END HACK
8785

@@ -110,8 +108,8 @@ class TulipApi {
110108
const response = await fetch(`${this.API_ENDPOINT}/tags`);
111109
const tags = await response.json();
112110

113-
// HACK: make starred look like a tag
114-
tags.push("starred")
111+
// HACK: make connected flows look like a tag
112+
tags.push("connected")
115113
// END HACK
116114

117115

@@ -129,8 +127,8 @@ class TulipApi {
129127
return await response.text()
130128
}
131129

132-
async toPythonRequest(body: string, tokenize: boolean) {
133-
const response = await fetch(`${this.API_ENDPOINT}/to_python_request?tokenize=${tokenize ? "1" : "0"}`, {
130+
async toPythonRequest(body: string, id: string, tokenize: boolean) {
131+
const response = await fetch(`${this.API_ENDPOINT}/to_python_request?tokenize=${tokenize ? "1" : "0"}&id=${id}`, {
134132
method: "POST",
135133
headers: {
136134
"Content-Type": "text/plain;charset=UTF-8"

frontend/src/components/FlowList.tsx

+28-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
useParams,
55
useNavigate,
66
} from "react-router-dom";
7-
import { useCallback, useEffect, useState } from "react";
7+
import { useCallback, useEffect, useState, useRef } from "react";
88
import { useAtom, useAtomValue } from "jotai";
99
import { Flow, FullFlow, useTulip } from "../api";
1010
import {
@@ -14,7 +14,7 @@ import {
1414
END_FILTER_KEY,
1515
} from "../App";
1616

17-
import { HeartIcon, FilterIcon } from "@heroicons/react/solid";
17+
import { HeartIcon, FilterIcon, LinkIcon } from "@heroicons/react/solid";
1818
import {
1919
HeartIcon as EmptyHeartIcon,
2020
FilterIcon as EmptyFilterIcon,
@@ -23,7 +23,7 @@ import {
2323
import classes from "./FlowList.module.css";
2424
import { format } from "date-fns";
2525
import useDebounce from "../hooks/useDebounce";
26-
import { Virtuoso } from "react-virtuoso";
26+
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
2727
import classNames from "classnames";
2828
import { Tag } from "./Tag";
2929
import { lastRefreshAtom } from "./Header";
@@ -36,6 +36,11 @@ export function FlowList() {
3636

3737
const [flowList, setFlowList] = useState<Flow[]>([]);
3838

39+
// Set default flowIndex to Infinity, so that initialTopMostItemIndex != 0 and therefore scrolledToInitialItem != true
40+
const [flowIndex, setFlowIndex] = useState<number>(Infinity);
41+
42+
const virtuoso = useRef<VirtuosoHandle>(null);
43+
3944
const service_name = searchParams.get(SERVICE_FILTER_KEY) ?? "";
4045
const service = services.find((s) => s.name == service_name);
4146

@@ -73,8 +78,13 @@ export function FlowList() {
7378
service: "", // FIXME
7479
tags: selectedTags,
7580
});
81+
data.forEach((flow, index)=> {
82+
if(flow._id.$oid === params.id){ setFlowIndex(index)}
83+
})
84+
7685
setFlowList(data);
7786
setLoading(false);
87+
7888
};
7989
fetchData().catch(console.error);
8090
}, [
@@ -84,12 +94,14 @@ export function FlowList() {
8494
to_filter,
8595
selectedTags,
8696
lastRefresh,
97+
params,
98+
virtuoso
8799
]);
88100

89101
const onHeartHandler = useCallback(async (flow: Flow) => {
90-
await api.starFlow(flow._id.$oid, !flow.starred);
102+
await api.starFlow(flow._id.$oid, !flow.tags.includes("starred"));
91103
// optimistic update
92-
const newFlow = { ...flow, starred: !flow.starred };
104+
const newFlow = { ...flow};
93105
setFlowList((prev) =>
94106
prev.map((f) => (f._id.$oid === flow._id.$oid ? newFlow : f))
95107
);
@@ -147,6 +159,8 @@ export function FlowList() {
147159
"sidebar-loading": loading,
148160
})}
149161
data={flowList}
162+
ref={virtuoso}
163+
initialTopMostItemIndex={flowIndex}
150164
itemContent={(index, flow) => (
151165
<Link
152166
to={`/flow/${flow._id.$oid}?${searchParams}`}
@@ -178,15 +192,14 @@ function FlowListEntry({ flow, isActive, onHeartClick }: FlowListEntryProps) {
178192

179193
const isStarred = flow.tags.includes("starred");
180194
// Filter tag list for tags that are handled specially
181-
const filtered_tag_list = flow.tags.filter((t) => !["starred"].includes(t));
195+
const filtered_tag_list = flow.tags.filter((t) => t != "starred");
182196

183197
const duration =
184198
flow.duration > 10000 ? (
185199
<div className="text-red-500">&gt;10s</div>
186200
) : (
187201
<div>{flow.duration}ms</div>
188202
);
189-
190203
return (
191204
<li
192205
className={classNames({
@@ -195,17 +208,23 @@ function FlowListEntry({ flow, isActive, onHeartClick }: FlowListEntryProps) {
195208
>
196209
<div className="flex">
197210
<div
198-
className="w-5 ml-2 mr-4 self-center shrink-0"
211+
className="w-5 ml-1 mr-1 self-center shrink-0"
199212
onClick={() => {
200213
onHeartClick(flow);
201214
}}
202215
>
203-
{flow.starred ? (
216+
{flow.tags.includes("starred") ? (
204217
<HeartIcon className="text-red-500" />
205218
) : (
206219
<EmptyHeartIcon />
207220
)}
208221
</div>
222+
223+
<div className="w-5 mr-2 self-center shrink-0">
224+
{flow.child_id.$oid != "000000000000000000000000" || flow.parent_id.$oid != "000000000000000000000000" ? (
225+
<LinkIcon className="text-blue-500" />
226+
) : undefined}
227+
</div>
209228
<div className="flex-1 shrink">
210229
<div className="flex">
211230
<div className="shrink-0">

frontend/src/pages/FlowView.tsx

+20-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useParams } from "react-router-dom";
1+
import { useSearchParams, Link, useParams } from "react-router-dom";
22
import React, { useEffect, useState } from "react";
33
import { useTulip, FlowData, FullFlow } from "../api";
44
import { Buffer } from "buffer";
@@ -84,14 +84,14 @@ function WebFlow({ flow }: { flow: FlowData }) {
8484
);
8585
}
8686

87-
function PythonRequestFlow({ flow }: { flow: FlowData }) {
87+
function PythonRequestFlow({ full_flow, flow }: {full_flow:FullFlow, flow: FlowData }) {
8888
const [data, setData] = useState("");
8989

9090
const { api } = useTulip();
9191

9292
useEffect(() => {
9393
const fetchData = async () => {
94-
const data = await api.toPythonRequest(btoa(flow.data), true);
94+
const data = await api.toPythonRequest(btoa(flow.data), full_flow._id.$oid,true);
9595
setData(data);
9696
};
9797
// TODO proper error handling
@@ -102,6 +102,7 @@ function PythonRequestFlow({ flow }: { flow: FlowData }) {
102102
}
103103

104104
interface FlowProps {
105+
full_flow: FullFlow;
105106
flow: FlowData;
106107
delta_time: number;
107108
}
@@ -140,7 +141,7 @@ function detectType(flow: FlowData) {
140141
return "Plain";
141142
}
142143

143-
function Flow({ flow, delta_time }: FlowProps) {
144+
function Flow({ full_flow, flow, delta_time }: FlowProps) {
144145
const formatted_time = format(new Date(flow.time), "HH:mm:ss:SSS");
145146
const displayOptions = ["Plain", "Hex", "Web", "PythonRequest"];
146147

@@ -183,7 +184,7 @@ function Flow({ flow, delta_time }: FlowProps) {
183184
{displayOption === "Plain" && <TextFlow flow={flow}></TextFlow>}
184185
{displayOption === "Web" && <WebFlow flow={flow}></WebFlow>}
185186
{displayOption === "PythonRequest" && (
186-
<PythonRequestFlow flow={flow}></PythonRequestFlow>
187+
<PythonRequestFlow flow={flow} full_flow={full_flow}></PythonRequestFlow>
187188
)}
188189
</div>
189190
</div>
@@ -263,6 +264,7 @@ function FlowOverview({ flow }: { flow: FullFlow }) {
263264
function Header() {}
264265

265266
export function FlowView() {
267+
let [searchParams] = useSearchParams();
266268
const params = useParams();
267269
const [flow, setFlow] = useState<FullFlow>();
268270

@@ -318,17 +320,30 @@ export function FlowView() {
318320
</button>
319321
</div>
320322
</div>
323+
{flow?.parent_id?.$oid != "000000000000000000000000"? <Link
324+
to={`/flow/${flow.parent_id.$oid}?${searchParams}`}
325+
key={flow.parent_id.$oid}
326+
className="focus-visible:rounded-md"
327+
>Parent</Link>: undefined}
328+
321329
{flow ? <FlowOverview flow={flow}></FlowOverview> : undefined}
322330
{flow?.flow.map((flow_data, i, a) => {
323331
const delta_time = a[i].time - (a[i - 1]?.time ?? a[i].time);
324332
return (
325333
<Flow
326334
flow={flow_data}
327335
delta_time={delta_time}
336+
full_flow={flow}
328337
key={flow._id.$oid + " " + i}
329338
></Flow>
330339
);
331340
})}
341+
342+
{flow?.child_id?.$oid != "000000000000000000000000" ? <Link
343+
to={`/flow/${flow.child_id.$oid}?${searchParams}`}
344+
key={flow.child_id.$oid}
345+
className="focus-visible:rounded-md"
346+
>Child</Link>: undefined}
332347
</div>
333348
);
334349
}

services/README.md

-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ Each document will have:
1818
"dst_ip": "127.0.0.1",
1919
"dst_port": 1234,
2020
"contains_flag": //true if the importer have found that the flow contains a flag based on the env var regex
21-
"starred": //if the flow is starred
2221
"flow": [
2322
{
2423
"data": "...", //printable data
@@ -59,9 +58,6 @@ Returns the all document with `flow_id` id, including the field `flow`
5958
##### GET /star/(flow_id)/(0,1)
6059
Set the flow favourite (1) or not (0)
6160

62-
##### POST /starred
63-
Returns a list of document like `/query` endpoint, but only with starred items.
64-
6561
##### POST /to_python_request/(tokenize)
6662
convert the request to python syntax. Tokenize is used to toggle the auto-parsing of args.
6763

services/data2req.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def send_error(self, code, message):
4848
self.error_message = message
4949

5050
# tokenize used for automatically fill data param of request
51-
def convert_http_requests(raw_request, tokenize=True, use_requests_session=False):
51+
def convert_http_requests(raw_request, flow, tokenize=True, use_requests_session=False):
5252
request = HTTPRequest(raw_request)
5353

5454
data = {}
@@ -96,8 +96,9 @@ def convert_http_requests(raw_request, tokenize=True, use_requests_session=False
9696

9797
rtemplate = Environment(loader=BaseLoader()).from_string("""import os
9898
import requests
99+
import sys
99100
100-
host = os.getenv("TARGET_IP")
101+
host = sys.argv[1]
101102
{% if use_requests_session %}
102103
s = requests.Session()
103104
@@ -107,12 +108,13 @@ def convert_http_requests(raw_request, tokenize=True, use_requests_session=False
107108
{% endif %}
108109
data = {{data}}
109110
110-
{% if use_requests_session %}s{% else %}requests{% endif %}.{{request.command.lower()}}("http://{}{{request.path}}".format(host), {{data_param_name}}=data{% if not use_requests_session %}, headers=headers{% endif %})""")
111+
{% if use_requests_session %}s{% else %}requests{% endif %}.{{request.command.lower()}}("http://{}:{{port}}{{request.path}}".format(host), {{data_param_name}}=data{% if not use_requests_session %}, headers=headers{% endif %})""")
111112

112113
return rtemplate.render(
113114
headers=str(dict(headers)),
114115
data=data,
115116
request=request,
116117
data_param_name=data_param_name,
117118
use_requests_session=use_requests_session,
119+
port=flow["dst_port"]
118120
)

services/db.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ def getFlowList(self, filters):
5959
if "from_time" in filters and "to_time" in filters:
6060
f["time"] = {"$gte": int(filters["from_time"]),
6161
"$lt": int(filters["to_time"])}
62-
if "starred" in filters:
63-
f["starred"] = bool(filters["starred"])
64-
if "blocked" in filters:
65-
f["blocked"] = bool(filters["blocked"])
6662
if "tags" in filters:
6763
f["tags"] = {
6864
"$all": [str(elem) for elem in filters["tags"]]
@@ -92,7 +88,10 @@ def getFlowDetail(self, id):
9288
return ret
9389

9490
def setStar(self, flow_id, star):
95-
self.pcap_coll.find_one_and_update({"_id": ObjectId(flow_id)}, {"$set": {"starred": star}})
91+
if star:
92+
self.pcap_coll.find_one_and_update({"_id": ObjectId(flow_id)}, {"$push": {"tags": "starred"}})
93+
else:
94+
self.pcap_coll.find_one_and_update({"_id": ObjectId(flow_id)}, {"$pull": {"tags": "starred"}})
9695

9796
def isFileAlreadyImported(self, file_name):
9897
return self.file_coll.find({"file_name": file_name}).count() != 0

services/flow2pwn.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ def flow2pwn(flow):
4040
port = flow["dst_port"]
4141

4242
script = """from pwn import *
43+
import sys
4344
44-
proc = remote('{}', {})
45+
host = sys.argv[1]
46+
proc = remote(host, {})
4547
""".format(ip, port)
4648

4749
for message in flow['flow']:

0 commit comments

Comments
 (0)