Skip to content

Commit a778d66

Browse files
authored
Merge pull request #40 from 0x707a15ec/teameurope/frontend-features
Added Suricata Docker deployment, enabled flow marking, some usability and documentation improvements
2 parents b8af07c + d2a978e commit a778d66

File tree

13 files changed

+294
-67
lines changed

13 files changed

+294
-67
lines changed

‎.env.example‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ TRAFFIC_DIR_HOST="./services/test_pcap"
1111
# The location of your pcaps (and eve.json), as seen by the container
1212
TRAFFIC_DIR_DOCKER="/traffic"
1313

14+
# Set BPF filter expression (see https://www.tcpdump.org/manpages/pcap-filter.7.html)
15+
#BPF="port 8080"
16+
1417
# Visualizer
15-
VISUALIZER_URL="http://scraper.example.com"
18+
#VISUALIZER_URL="http://scraper.example.com"
1619

1720
##############################
1821
# Game config
@@ -42,6 +45,7 @@ PCAP_OVER_IP=
4245
#PCAP_OVER_IP="host.docker.internal:1337"
4346
# For multiple PCAP_OVER_IP you can comma separate
4447
#PCAP_OVER_IP="host.docker.internal:1337,otherhost.com:5050"
48+
<<<<<<< HEAD
4549

4650
##############################
4751
# DUMP_PCAPS CONFIGS
@@ -95,3 +99,11 @@ FLAG_VALIDATOR_TYPE=
9599
# Some flag validators can make use of (our) team number/ID
96100
# Ignored unless FLAG_VALIDATOR_TYPE is set
97101
FLAG_VALIDATOR_TEAM=42
102+
103+
##############################
104+
# SURICATA CONFIGS
105+
##############################
106+
107+
# Directory for Suricata files (see suricata/etc, suricata/lib/rules, suricata/logs)
108+
# (they should be generated on first run)
109+
#SURICATA_DIR_HOST="./suricata"

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,4 @@ workspace.xml
132132
.idea
133133

134134
/traffic
135+
suricata

‎README.md‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ Tulip was developed by Team Europe for use in the first International Cyber Secu
1313
* Synchronized with Suricata.
1414
* Flow diffing
1515
* Time and size-based plots for correlation.
16-
* Linking HTTP sessions together based on cookies (Experimental, disabled by default)
16+
* Linking HTTP sessions together based on cookies (Experimental*, disabled by default)
17+
* PCAP-over-IP with BPF filtering support**
18+
19+
\* - to enable, add `-experimental` after `./assembler` in `docker-compose.yml`
20+
21+
\*\* - to enable, configure PCAP-over-IP server (e.g. [pcap-broker](https://github.com/fox-it/pcap-broker) as suggested in [PR 24](https://github.com/OpenAttackDefenseTools/tulip/pull/24)) and set `PCAP_OVER_IP` (and `BPF` if necessary) in `.env`
1722

1823
## Screenshots
1924
![](./demo_images/demo1.png)
@@ -45,21 +50,54 @@ docker-compose up -d --build
4550
```
4651
To ingest traffic, it is recommended to create a shared bind mount with the docker-compose. One convenient way to set this up is as follows:
4752
1. On the vulnbox, start a rotating packet sniffer (e.g. tcpdump, suricata, ...)
48-
1. Using rsync, copy complete captures to the machine running tulip (e.g. to /traffic)
49-
1. Add a bind to the assembler service so it can read /traffic
53+
```bash
54+
tcpdump -i eth0 -G 180 -w "traffic_%H:%M:%S.pcap" port 8080
55+
```
56+
2. Using rsync, copy complete captures to the machine running tulip (e.g. to /traffic)
57+
```bash
58+
rsync -avz -e ssh --progress root@10.0.0.2:/pcaps ./pcaps
59+
```
60+
3. Add a bind to the assembler service so it can read /traffic
61+
> (Just change `TRAFFIC_DIR_HOST` in `.env`)
5062
5163
The ingestor will use inotify to watch for new pcap's and suricata logs. No need to set a chron job.
5264

5365

5466
## Suricata synchronization
5567

68+
### Run in Docker
69+
70+
Configure `SURICATA_DIR_HOST` in `.env`.
71+
72+
Create some rules (404 for testing):
73+
```bash
74+
. .env
75+
mkdir -p ${SURICATA_DIR_HOST}/{etc,lib/rules,log}
76+
echo 'alert tcp any any -> any any (msg: "404 Not Found"; http.stat_code; content:"404"; metadata: tag notfound; sid:4; rev: 1;)' >> ${SURICATA_DIR_HOST}/lib/rules/suricata.rules
77+
```
78+
79+
After that run (default config for `eve.json` logging was good enough):
80+
81+
```bash
82+
docker compose -f docker-compose-suricata.yml up -d --build
83+
```
84+
5685
### Metadata
5786
Tags are read from the metadata field of a rule. For example, here's a simple rule to detect a path traversal:
5887
```
5988
alert tcp any any -> any any (msg: "Path Traversal-../"; flow:to_server; content: "../"; metadata: tag path_traversal; sid:1; rev: 1;)
6089
```
6190
Once this rule is seen in traffic, the `path_traversal` tag will automatically be added to the filters in Tulip.
6291

92+
> [!NOTE]
93+
>
94+
> After editing Suricata rules (renaming or id change) please:
95+
>
96+
> Remove old logs: `rm ${SURICATA_DIR_HOST}/log/*` (otherwise old signatures will be repopulated).
97+
>
98+
> Restart Docker containers.
99+
>
100+
> If database was only restarted (not dropped), try cleaning tags/signatures manually.
63101
64102
### eve.json
65103
Suricata alerts are read directly from the `eve.json` file. Because this file can get quite verbose when all extensions are enabled, it is recommended to strip the config down a fair bit. For example:
@@ -88,7 +126,6 @@ Your Tulip instance will probably contain sensitive CTF information, like flags
88126

89127
# Contributing
90128
If you have an idea for a new feature, bug fixes, UX improvements, or other contributions, feel free to open a pull request or create an issue!
91-
When opening a pull request, please target the `devel` branch.
92129

93130
# Credits
94131
Tulip was written by [@RickdeJager](https://github.com/rickdejager) and [@Bazumo](https://github.com/bazumo), with additional help from [@Sijisu](https://github.com/sijisu). Thanks to our fellow Team Europe players and coaches for testing, feedback and suggestions. Finally, thanks to the team behind [flower](https://github.com/secgroup/flower) for opensourcing their tooling.

‎docker-compose-suricata.yml‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
version: "3.5"
2+
services:
3+
timescale:
4+
build: services/timescale
5+
image: tulip-timescale:latest
6+
restart: unless-stopped
7+
volumes:
8+
- timescale-data:/var/lib/postgresql/data
9+
- ./services/schema/system.sql:/docker-entrypoint-initdb.d/100_system.sql:ro
10+
- ./services/schema/functions.sql:/docker-entrypoint-initdb.d/101_functions.sql:ro
11+
- ./services/schema/schema.sql:/docker-entrypoint-initdb.d/102_schema.sql:ro
12+
networks:
13+
- internal
14+
environment:
15+
POSTGRES_HOST_AUTH_METHOD: trust
16+
POSTGRES_USER: tulip
17+
POSTGRES_DB: tulip
18+
# This does not need to be adjusted, unless you actually want to limit it
19+
# Postgres uses shared memory for caching, and docker assigns just 64 MB by default
20+
shm_size: '128g'
21+
22+
frontend:
23+
build:
24+
context: frontend
25+
dockerfile: Dockerfile-frontend
26+
image: tulip-frontend:latest
27+
restart: unless-stopped
28+
ports:
29+
- "3000:3000"
30+
expose:
31+
- 3000
32+
depends_on:
33+
- timescale
34+
- api
35+
networks:
36+
- internal
37+
environment:
38+
API_SERVER_ENDPOINT: http://api:5000/
39+
VIRTUAL_HOST: tulip.h4xx.eu
40+
41+
api:
42+
build:
43+
context: services/api
44+
dockerfile: Dockerfile-api
45+
image: tulip-api:latest
46+
restart: unless-stopped
47+
depends_on:
48+
- timescale
49+
networks:
50+
- internal
51+
volumes:
52+
- ${TRAFFIC_DIR_HOST}:${TRAFFIC_DIR_DOCKER}:ro
53+
environment:
54+
TIMESCALE: ${TIMESCALE}
55+
TULIP_TRAFFIC_DIR: ${TRAFFIC_DIR_DOCKER}
56+
FLAG_REGEX: ${FLAG_REGEX}
57+
TICK_START: ${TICK_START}
58+
TICK_LENGTH: ${TICK_LENGTH}
59+
VM_IP: ${VM_IP}
60+
61+
flagids:
62+
restart: unless-stopped
63+
build:
64+
context: services/flagids
65+
image: tulip-flagids:latest
66+
depends_on:
67+
- timescale
68+
networks:
69+
- internal
70+
environment:
71+
TIMESCALE: ${TIMESCALE}
72+
TICK_START: ${TICK_START}
73+
TICK_LENGTH: ${TICK_LENGTH}
74+
FLAGID_SCRAPE: ${FLAGID_SCRAPE}
75+
TEAM_ID: ${TEAM_ID}
76+
FLAGID_ENDPOINT: ${FLAGID_ENDPOINT}
77+
VISUALIZER_URL: ${VISUALIZER_URL}
78+
DUMP_PCAPS: ${DUMP_PCAPS}
79+
80+
assembler:
81+
build:
82+
context: services/go-importer
83+
dockerfile: Dockerfile-assembler
84+
image: tulip-assembler:latest
85+
restart: unless-stopped
86+
depends_on:
87+
- timescale
88+
networks:
89+
- internal
90+
volumes:
91+
- ${TRAFFIC_DIR_HOST}:${TRAFFIC_DIR_DOCKER}:ro,z
92+
# Command line flags most likely to fix a tulip issue:
93+
# - -http-session-tracking: enable HTTP session tracking
94+
# - -dir: directory to read traffic from
95+
# - -skipchecksum: skip checksum validation
96+
# - -flush-after: i.e. 2m Not needed in pcap rotation mode
97+
# - -disable-converters: disable converters
98+
# - -discard-extra-data: dont split large flow items, just discard them
99+
command: "./assembler -http-session-tracking -skipchecksum -disable-converters -dir ${TRAFFIC_DIR_DOCKER}"
100+
environment:
101+
TIMESCALE: ${TIMESCALE}
102+
FLAG_REGEX: ${FLAG_REGEX}
103+
TICK_START: ${TICK_START}
104+
TICK_LENGTH: ${TICK_LENGTH}
105+
FLAGID_SCAN: ${FLAGID_SCAN}
106+
FLAG_LIFETIME: ${FLAG_LIFETIME}
107+
FLAG_VALIDATOR_TYPE: ${FLAG_VALIDATOR_TYPE}
108+
FLAG_VALIDATOR_TEAM: ${FLAG_VALIDATOR_TEAM}
109+
PCAP_OVER_IP: ${PCAP_OVER_IP}
110+
DUMP_PCAPS: ${DUMP_PCAPS}
111+
DUMP_PCAPS_INTERVAL: ${DUMP_PCAPS_INTERVAL}
112+
DUMP_PCAPS_FILENAME: ${DUMP_PCAPS_FILENAME}
113+
BPF: ${BPF}
114+
extra_hosts:
115+
- "host.docker.internal:host-gateway"
116+
117+
enricher:
118+
build:
119+
context: services/go-importer
120+
dockerfile: Dockerfile-enricher
121+
image: tulip-enricher:latest
122+
restart: unless-stopped
123+
depends_on:
124+
- timescale
125+
networks:
126+
- internal
127+
volumes:
128+
- ${SURICATA_DIR_HOST}/log:/suricata
129+
- ${TRAFFIC_DIR_HOST}:${TRAFFIC_DIR_DOCKER}:ro
130+
command: "./enricher -eve /suricata/eve.json"
131+
environment:
132+
TIMESCALE: ${TIMESCALE}
133+
134+
suricata:
135+
image: jasonish/suricata:7.0
136+
restart: unless-stopped
137+
volumes:
138+
- ${TRAFFIC_DIR_HOST}:${TRAFFIC_DIR_DOCKER}:ro
139+
- ${SURICATA_DIR_HOST}/log:/var/log/suricata
140+
- ${SURICATA_DIR_HOST}/etc:/etc/suricata
141+
- ${SURICATA_DIR_HOST}/lib:/var/lib/suricata
142+
environment:
143+
SURICATA_OPTIONS: "-l /var/log/suricata -v -r ${TRAFFIC_DIR_DOCKER} --pcap-file-continuous"
144+
145+
volumes:
146+
timescale-data:
147+
148+
networks:
149+
internal:

‎docker-compose.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ services:
110110
DUMP_PCAPS: ${DUMP_PCAPS}
111111
DUMP_PCAPS_INTERVAL: ${DUMP_PCAPS_INTERVAL}
112112
DUMP_PCAPS_FILENAME: ${DUMP_PCAPS_FILENAME}
113+
BPF: ${BPF}
113114
extra_hosts:
114115
- "host.docker.internal:host-gateway"
115116

‎frontend/src/api.ts‎

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -166,28 +166,28 @@ export const tulipApi = createApi({
166166
}),
167167
// TODO: optimistic cache update
168168

169-
// async onQueryStarted({ id, star }, { dispatch, queryFulfilled }) {
170-
// // `updateQueryData` requires the endpoint name and cache key arguments,
171-
// // so it knows which piece of cache state to update
172-
// const patchResult = dispatch(
173-
// tulipApi.util.updateQueryData("getFlows", undefined, (flows) => {
174-
// // The `flows` is Immer-wrapped and can be "mutated" like in createSlice
175-
// const flow = flows.find((flow) => flow._id.$oid === id);
176-
// if (flow) {
177-
// if (star) {
178-
// flow.tags.push("starred");
179-
// } else {
180-
// flow.tags = flow.tags.filter((tag) => tag != "starred");
181-
// }
182-
// }
183-
// })
184-
// );
185-
// try {
186-
// await queryFulfilled;
187-
// } catch {
188-
// patchResult.undo();
189-
// }
190-
// },
169+
async onQueryStarted({ id, star }, { dispatch, queryFulfilled }) {
170+
// `updateQueryData` requires the endpoint name and cache key arguments,
171+
// so it knows which piece of cache state to update
172+
const patchResult = dispatch(
173+
tulipApi.util.updateQueryData("getFlows", {service: "undefined", tags_include: [], tags_exclude:[]}, (flows) => {
174+
// The `flows` is Immer-wrapped and can be "mutated" like in createSlice
175+
const flow = flows.find((flow) => flow.id === id);
176+
if (flow) {
177+
if (star) {
178+
flow.tags.push("starred");
179+
} else {
180+
flow.tags = flow.tags.filter((tag) => tag != "starred");
181+
}
182+
}
183+
})
184+
);
185+
try {
186+
await queryFulfilled;
187+
} catch {
188+
patchResult.undo();
189+
}
190+
},
191191
}),
192192
}),
193193
});

‎frontend/src/components/Corrie.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export const Corrie = () => {
190190
>
191191
under attack
192192
</button>
193+
<p className="text-left px-2 py-2">After clicking on a flow, press 'w' to scroll to it in flow list</p>
193194
</div>
194195
</div>
195196
<div className="flex-1 w-full overflow-hidden p-4">

‎frontend/src/components/FlowList.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
START_FILTER_KEY,
1515
END_FILTER_KEY,
1616
FLOW_LIST_REFETCH_INTERVAL_MS,
17+
FORCE_REFETCH_ON_STAR,
1718
} from "../const";
1819
import { useAppSelector, useAppDispatch } from "../store";
1920
import { toggleFilterTag, toggleTagIntersectMode } from "../store/filter";
@@ -118,6 +119,7 @@ export function FlowList() {
118119

119120
const onHeartHandler = async (flow: Flow) => {
120121
await starFlow({ id: flow.id, star: !flow.tags.includes("starred") });
122+
if(FORCE_REFETCH_ON_STAR) refetch();
121123
};
122124

123125
const navigate = useNavigate();
@@ -164,7 +166,30 @@ export function FlowList() {
164166
[transformedFlowData]
165167
)
166168

169+
useHotkeys('x', async () => {
170+
if(transformedFlowData) {
171+
let flow = transformedFlowData[flowIndex ?? 0]
172+
await onHeartHandler(flow);
173+
}
174+
})
175+
167176
useHotkeys('j', () => setFlowIndex(fi => Math.min((transformedFlowData?.length ?? 1)-1, fi + 1)), [transformedFlowData?.length]);
177+
useHotkeys('w', () => {
178+
if(transformedFlowData) {
179+
let idAtIndex = transformedFlowData[flowIndex ?? 0].id;
180+
if (idAtIndex != openedFlowID) {
181+
let flowids = flowData?.map((flow, idx) => ([flow.id, idx]))
182+
if (flowids) {
183+
let found = flowids.filter((el)=>(el[0] == openedFlowID))
184+
if (found.length > 0) {
185+
let n = Number(found[0][1])
186+
setFlowIndex(n)
187+
}
188+
}
189+
}
190+
}
191+
}
192+
);
168193
useHotkeys('k', () => setFlowIndex(fi => Math.max(0, fi - 1)));
169194
useHotkeys('i', () => {
170195
setShowFilters(true)
@@ -178,6 +203,12 @@ export function FlowList() {
178203
dispatch(toggleFilterTag("flag-out"))
179204
}
180205
}, [availableTags]);
206+
useHotkeys('t', () => {
207+
setShowFilters(true)
208+
if ((availableTags ?? []).includes("starred")) {
209+
dispatch(toggleFilterTag("starred"))
210+
}
211+
}, [availableTags]);
181212
useHotkeys('r', () => refetch());
182213

183214
const [showFilters, setShowFilters] = useState(false);

0 commit comments

Comments
 (0)