Skip to content

Commit 0e9fea8

Browse files
committed
Block changing MAC mid-Labeling, more human readable PCAP files
1 parent a8dec0c commit 0e9fea8

File tree

4 files changed

+107
-85
lines changed

4 files changed

+107
-85
lines changed

src/libinspector/common.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ def bar_graph_data_frame(mac_address: str, now: int):
140140

141141
def plot_traffic_volume(df: pd.DataFrame, now: int, chart_title: str):
142142
"""
143-
Plots the traffic volume over time.
143+
Plots the traffic volume over time. The bar goes from right to left,
144+
like Task Manager in Windows.
144145
145146
Args:
146147
df (pd.DataFrame): DataFrame containing 'Time' and 'Bits' columns.
@@ -152,9 +153,9 @@ def plot_traffic_volume(df: pd.DataFrame, now: int, chart_title: str):
152153
else:
153154
st.markdown(f"#### {chart_title}")
154155
df['seconds_ago'] = now - df['timestamp'].astype(int)
155-
df = df.set_index('seconds_ago').reindex(range(0, 60), fill_value=0).reset_index()
156-
st.bar_chart(df.set_index('seconds_ago')['Bits'], width='content')
157-
156+
df_reindexed = df.set_index('seconds_ago').reindex(range(0, 60), fill_value=0).reset_index()
157+
df_reindexed = df_reindexed.sort_values(by='seconds_ago', ascending=False)
158+
st.bar_chart(df_reindexed.set_index('seconds_ago')['Bits'], width='content')
158159

159160
def get_device_metadata(mac_address: str) -> dict:
160161
"""

src/libinspector/device_detail_page.py

Lines changed: 89 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def show():
3333
- device details (A bar chart of traffic volume in the last 60 seconds, and a table of network flows)
3434
"""
3535
device_mac_address = show_device_list()
36-
36+
# The logic for displaying the API message has been moved into this fragment,
37+
display_api_status()
3738
if not device_mac_address:
3839
st.warning("No device selected. Please select a device to view details.")
3940
return
@@ -42,50 +43,26 @@ def show():
4243
show_device_details(device_mac_address)
4344

4445

45-
def reset_labeling_state():
46-
"""Resets all state variables related to a labeling session."""
47-
common.config_set('labeling_in_progress', False)
48-
st.session_state['countdown'] = False
49-
st.session_state['start_time'] = None
50-
st.session_state['end_time'] = None
51-
st.session_state['show_labeling_setup'] = False
52-
53-
54-
def _collect_packets_callback():
55-
"""
56-
Executed when 'Start' is clicked. Sets the flag to trigger the countdown
57-
and packet start logic in the main function.
58-
"""
59-
logger.info("[Packets] Start button clicked, initiating countdown and add to Label Deque...")
60-
st.session_state['countdown'] = True
61-
62-
labeling_event = {
63-
"prolific_id": common.config_get('prolific_id', ''),
64-
"device_name": st.session_state['device_name'],
65-
"activity_label": st.session_state['activity_label'],
66-
"mac_address": st.session_state['mac_address'],
67-
}
68-
_labeling_event_deque.append(labeling_event)
69-
# st.rerun() will be triggered by Streamlit after the callback completes
70-
71-
72-
def _send_packets_callback():
46+
@st.fragment(run_every=10)
47+
def display_api_status():
7348
"""
74-
Executed when the 'Labeling Complete' button is clicked.
75-
Handles stopping packet collection and sending data to the server.
49+
Continuously checks the global 'api_message' configuration variable
50+
and displays the corresponding Streamlit notification (success, error, or warning).
51+
This fragment runs every 10 seconds, ensuring the user sees updates from the
52+
background thread (label_thread) in real-time.
7653
"""
77-
if not common.config_get('labeling_in_progress', default=False):
78-
common.config_set('api_message', "warning|No labeling session in progress. Please start a labeling session first.")
79-
return
80-
81-
logger.info("[Packets] Collect the end time and prep for packet collection.")
82-
if len(_labeling_event_deque) == 0:
83-
logger.warning("[Packets] No labeling event found in the queue when trying to set end time.")
84-
else:
85-
_labeling_event_deque[-1]['end_time'] = int(time.time())
86-
st.session_state['end_time'] = _labeling_event_deque[-1]['end_time']
54+
# Use st.empty() to ensure the message appears in the same place and is cleared on next run
55+
status_placeholder = st.empty()
56+
api_message = common.config_get('api_message', default='')
8757

88-
reset_labeling_state()
58+
if len(api_message) != 0:
59+
msg_type, msg = api_message.split('|', 1)
60+
if msg_type == 'success':
61+
status_placeholder.success(f"✅ {msg}")
62+
elif msg_type == 'error':
63+
status_placeholder.error(f"❌ {msg}")
64+
elif msg_type == 'warning':
65+
status_placeholder.warning(f"⚠️ {msg}")
8966

9067

9168
def label_thread():
@@ -101,7 +78,6 @@ def label_thread():
10178
logger.info("[Packets] Will check if there are labeled packets to send...")
10279
# 1. Check if labeling session has ended
10380
if len(_labeling_event_deque) == 0:
104-
common.config_set('api_message', "success| The packet collector thread is idle.")
10581
logger.info("[Packets] There is no labeling event ready. Not sending packets.")
10682
continue
10783
else:
@@ -110,14 +86,12 @@ def label_thread():
11086
# Make sure end time is NOT a none
11187
end_time = _labeling_event_deque[0].get('end_time', None)
11288
if end_time is None:
113-
common.config_set('api_message', "success| The packet collector thread is idle.")
11489
logger.info("[Packets] End time hasn't been set yet, The labeling session is still ongoing. Not sending packets yet.")
11590
continue
11691

11792
# If the labeling session is still ongoing, skip sending
11893
if time.time() <= end_time:
11994
logger.info("[Packets] Labeling session not complete yet. The end time is still in the future.")
120-
common.config_set('api_message', "warning| The packet collector thread is waiting for the labeling session to complete.")
12195
continue
12296

12397
# 2. Process and empty the queue
@@ -158,8 +132,9 @@ def label_thread():
158132
timeout=10
159133
)
160134
if response.status_code == 200:
161-
logger.info(f"[Packets] {time.strftime('%Y-%m-%d %H:%M:%S')} - All packets sent successfully.")
162-
common.config_set('api_message', f"success| Labeled packets successfully sent to the server. \n {label_data}")
135+
logger.info(f"[Packets] {time.strftime('%Y-%m-%d %H:%M:%S')} - All packets sent successfully. \n {label_data}")
136+
display_message = f"Labeled packets successfully | {label_data}"
137+
common.config_set('api_message', f"success|{display_message}")
163138
else:
164139
logger.info(f"[Packets] {time.strftime('%Y-%m-%d %H:%M:%S')} - API Failed, packets NOT sent!.")
165140
common.config_set('api_message', f"error|Failed to send labeled packets. Server status: {response.status_code}. {len(pending_packet_list)} Packets were not sent.")
@@ -173,6 +148,60 @@ def label_thread():
173148
pending_packet_list.clear()
174149

175150

151+
def reset_labeling_state():
152+
"""
153+
Resets all state variables related to a labeling session.
154+
"""
155+
st.session_state['countdown'] = False
156+
st.session_state['show_labeling_setup'] = False
157+
st.session_state['start_time'] = None
158+
st.session_state['end_time'] = None
159+
st.session_state['device_name'] = None
160+
st.session_state['activity_label'] = None
161+
162+
163+
def _collect_packets_callback():
164+
"""
165+
Executed when 'Start' is clicked. Sets the flag to trigger the countdown
166+
and packet start logic in the main function.
167+
"""
168+
logger.info("[Packets] Start button clicked, initiating countdown and add to Label Deque...")
169+
st.session_state['countdown'] = True
170+
171+
labeling_event = {
172+
"prolific_id": common.config_get('prolific_id', ''),
173+
"device_name": st.session_state['device_name'],
174+
"activity_label": st.session_state['activity_label'],
175+
"mac_address": st.session_state['mac_address'],
176+
}
177+
_labeling_event_deque.append(labeling_event)
178+
179+
180+
def _send_packets_callback():
181+
"""
182+
Executed when the 'Labeling Complete' button is clicked.
183+
Handles stopping packet collection and sending data to the server.
184+
"""
185+
if not common.config_get('labeling_in_progress', default=False):
186+
common.config_set('api_message', "warning|No labeling session in progress. Please start a labeling session first.")
187+
return
188+
189+
if common.config_get('packet_count', 0) == 0:
190+
st.warning("[Packets] No packets were captured for labeling.")
191+
logger.warning("[Packets] No packets were captured for labeling.")
192+
return
193+
194+
logger.info("[Packets] Collect the end time and prep for packet collection.")
195+
if len(_labeling_event_deque) == 0:
196+
logger.warning("[Packets] No labeling event found in the queue when trying to set end time.")
197+
else:
198+
_labeling_event_deque[-1]['end_time'] = int(time.time())
199+
st.session_state['end_time'] = _labeling_event_deque[-1]['end_time']
200+
common.config_set('labeling_in_progress', False)
201+
common.config_set('packet_count', 0)
202+
reset_labeling_state()
203+
204+
176205
def update_device_inspected_status(mac_address: str):
177206
"""
178207
Manually update to inspected status so that all the packets can be collected for the MAC Address.
@@ -191,28 +220,6 @@ def update_device_inspected_status(mac_address: str):
191220
common.config_set(device_inspected_config_key, True)
192221

193222

194-
@st.fragment(run_every=10)
195-
def display_api_status():
196-
"""
197-
Continuously checks the global 'api_message' configuration variable
198-
and displays the corresponding Streamlit notification (success, error, or warning).
199-
This fragment runs every 10 seconds, ensuring the user sees updates from the
200-
background thread (label_thread) in real-time.
201-
"""
202-
# Use st.empty() to ensure the message appears in the same place and is cleared on next run
203-
status_placeholder = st.empty()
204-
api_message = common.config_get('api_message', default='')
205-
206-
if len(api_message) != 0:
207-
msg_type, msg = api_message.split('|', 1)
208-
if msg_type == 'success':
209-
status_placeholder.success(f"✅ {msg}")
210-
elif msg_type == 'error':
211-
status_placeholder.error(f"❌ {msg}")
212-
elif msg_type == 'warning':
213-
status_placeholder.warning(f"⚠️ {msg}")
214-
215-
216223
def label_activity_workflow(mac_address: str):
217224
"""
218225
Manages the interactive, state-driven workflow for labeling network activity in Streamlit.
@@ -252,12 +259,12 @@ def label_activity_workflow(mac_address: str):
252259
help="Click to start labeling an activity for this device. This will reset any previous labeling state."
253260
):
254261
if len(_labeling_event_deque) > 0:
255-
st.warning("A previous labeling session is still active. Try again in 15 seconds when the previous session should have been sent.")
262+
st.warning("A previous labeling session is still active. Try again in 15 seconds when the previous session's packets should have been sent.")
256263
return
257264
update_device_inspected_status(mac_address)
258-
reset_labeling_state()
259265
# Keep labeling_in_progress=True until the very end, controlled by config_get/set
260266
common.config_set('labeling_in_progress', True)
267+
common.config_set('api_message', '')
261268
st.session_state['show_labeling_setup'] = True
262269
st.rerun() # Rerun to show setup menu
263270

@@ -322,9 +329,6 @@ def label_activity_workflow(mac_address: str):
322329
help="Click to stop collecting packets and send the labeled packets to NYU mLab."
323330
)
324331

325-
# The logic for displaying the API message has been moved into this fragment,
326-
display_api_status()
327-
328332
# --- 4. Countdown Logic (Blocking, happens between Start and Collection) ---
329333
if st.session_state['countdown']:
330334
countdown_placeholder = st.empty()
@@ -377,6 +381,9 @@ def save_labeled_activity_packets(pkt):
377381
if mac_address and (pkt[sc.Ether].src == mac_address or pkt[sc.Ether].dst == mac_address):
378382
# We also check if the timestamp is correct
379383
_labeled_activity_packet_queue.put(pkt)
384+
packet_count = common.config_get('packet_count', 0)
385+
packet_count += 1
386+
common.config_set('packet_count', packet_count)
380387

381388

382389
@st.cache_data(show_spinner=False)
@@ -523,6 +530,8 @@ def show_device_list():
523530
st.warning("No devices found. Please inspect a device first.")
524531
st.stop()
525532

533+
is_labeling_active = common.config_get('labeling_in_progress', default=False)
534+
526535
# Show a dropdown to select a device; each option shows both the device's MAC address and IP address
527536
device_options = [f"{common.get_device_custom_name(device['mac_address'])} - {device['ip_address']} - {device['mac_address']}" for device in device_list]
528537
device_options.insert(0, "(Select a device)") # Add a placeholder option
@@ -550,8 +559,13 @@ def _selected_device_changed_callback():
550559
device_options,
551560
index=selected_index,
552561
key='selected_device',
553-
on_change=_selected_device_changed_callback
562+
on_change=_selected_device_changed_callback,
563+
disabled=is_labeling_active
554564
)
555565

566+
if is_labeling_active:
567+
st.warning(
568+
"⚠️ **Device selection is locked** while a labeling session is in progress.")
569+
556570
return device_mac_address
557571

src/libinspector/page_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def start_inspector_once():
7575
# Same with the general warning
7676
common.config_set("suppress_warning", False)
7777
common.config_set("labeling_in_progress", False)
78+
common.config_set("api_message", "")
7879
libinspector.core.start_threads()
7980
api_thread = threading.Thread(
8081
name="Device API Thread",

src/libinspector/server/packet_collector.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,20 +173,26 @@ def label_packets():
173173

174174
def make_pcap_filename(start_time: int, end_time: int) -> str:
175175
"""
176-
Generates a pcap filename using the start and end epoch timestamps.
176+
Generates a pcap filename that is both human-readable and informative.
177+
The format is: 'Mon-DD-YYYY_HHMMSSAM/PM_DurationSeconds.pcap'
178+
179+
Example: 'Oct-31-2025_11:31:00AM_6s.pcap'
177180
178181
Args:
179182
start_time (int): Start time in seconds since the epoch.
180183
end_time (int): End time in seconds since the epoch.
181184
182185
Returns:
183-
str: Filename in the format 'YYYYMMDD_HHMMSS-YYYYMMDD_HHMMSS.pcap'.
186+
str: Human-readable filename.
184187
"""
185188
start_dt = datetime.datetime.fromtimestamp(start_time)
186-
end_dt = datetime.datetime.fromtimestamp(end_time)
187-
safe_start = start_dt.strftime("%Y%m%d_%H%M%S")
188-
safe_end = end_dt.strftime("%Y%m%d_%H%M%S")
189-
filename = f"{safe_start}-{safe_end}.pcap"
189+
duration_seconds = end_time - start_time
190+
191+
# Format the start time: e.g., 'Oct-31-2025_113100AM'
192+
safe_start = start_dt.strftime("%b-%d-%Y_%I:%M:%S%p")
193+
194+
# Generate the filename with duration
195+
filename = f"{safe_start}_{duration_seconds}s.pcap"
190196
return filename
191197

192198

0 commit comments

Comments
 (0)