Skip to content

Commit b6f2d5e

Browse files
authored
Merge pull request #266 from nyu-mlab/ui
More UI Fixes
2 parents 363e82b + 18f5916 commit b6f2d5e

File tree

8 files changed

+102
-56
lines changed

8 files changed

+102
-56
lines changed

.github/labeler.yml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ Github-files:
55
- '.github/**'
66

77
Tests:
8-
- 'tests/**'
9-
10-
Analysis:
11-
- 'analysis/**'
8+
- 'src/tests/**'
129

1310
Core:
14-
- 'core/**'
11+
- 'src/libinspector/**.py'
12+
- 'pyproject.toml'
13+
- 'setup.py'
14+
- 'uv.lock'
15+
16+
Scripts:
17+
- '**/*.sh'
18+
- '**/*.ps1'
19+
- '**/*.bat'
1520

1621
Data:
17-
- 'data/**'
22+
- 'src/libinspector/data/**'
1823

19-
UI:
20-
- 'ui/**'
24+
Streamlit:
25+
- 'src/libinspector/.streamlit/**'
26+
- '.streamlit/**'

.github/workflows/create_release.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ on:
66
branches:
77
- master
88

9+
# Set no permissions by default. This is the most secure practice.
10+
# We will grant specific permissions to each job that needs them.
11+
permissions: {}
12+
913
jobs:
1014
create_release:
1115
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
1218
outputs:
1319
v-version: ${{ steps.version.outputs.v-version }}
1420
steps:
@@ -22,6 +28,8 @@ jobs:
2228

2329
build:
2430
runs-on: ${{ matrix.os }}
31+
permissions:
32+
contents: read
2533
needs: [create_release]
2634
strategy:
2735
fail-fast: false
@@ -101,6 +109,9 @@ jobs:
101109
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
102110
with:
103111
tag_name: ${{ needs.create_release.outputs.v-version }}
112+
draft: false
113+
generate_release_notes: true
114+
prerelease: false
104115
files: |
105116
dist/*.exe
106117
dist/*.whl

.github/workflows/inspector_test.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ on:
77
schedule:
88
- cron: '0 0 */30 * *'
99

10+
# Set no permissions by default. This is the most secure practice.
11+
# We will grant specific permissions to each job that needs them.
12+
permissions: {}
13+
1014
jobs:
1115
build:
1216
runs-on: ${{ matrix.os }}
17+
permissions:
18+
contents: read
1319
strategy:
1420
fail-fast: false
1521
matrix:

src/libinspector/common.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@
2626
"- Metadata of your network traffic (e.g., which IPs/domains your devices communicate with) is shared anonymously with NYU researchers during the labeling stage."
2727
)
2828

29+
def remove_warning():
30+
"""
31+
Remove the warning acceptance state, forcing the user to see the warning again.
32+
"""
33+
config_set("suppress_warning", True)
34+
35+
36+
def reset_prolific_id():
37+
"""
38+
Clear the stored Prolific ID, forcing the user to re-enter it.
39+
"""
40+
config_set("prolific_id", "")
41+
42+
43+
def set_prolific_id(prolific_id: str):
44+
"""
45+
Store the provided Prolific ID in the configuration.
46+
47+
Args:
48+
prolific_id (str): The Prolific ID to store.
49+
"""
50+
if is_prolific_id_valid(prolific_id):
51+
config_set("prolific_id", prolific_id)
52+
53+
2954
def show_warning():
3055
"""
3156
Displays a warning message to the user about network monitoring and ARP spoofing.
@@ -36,28 +61,30 @@ def show_warning():
3661
False if the user has accepted the warning and can proceed.
3762
"""
3863
current_id = config_get("prolific_id", "")
64+
st.subheader("1. Prolific ID Confirmation")
65+
st.info(f"Your currently stored ID is: `{current_id}`")
66+
st.button("Change Prolific ID",
67+
on_click=reset_prolific_id,
68+
help="Clicking this will clear your stored ID and return you to the ID entry form.")
3969

4070
# --- GATE 1: PROLIFIC ID CHECK (Must be valid to proceed to confirmation) ---
4171
if is_prolific_id_valid(current_id):
42-
# --- SHOW CONFIRMATION UI (Only reached if ID is valid but warning is unaccepted) ---
43-
st.subheader("1. Prolific ID Confirmation")
44-
st.info(f"Your currently stored ID is: `{current_id}`")
45-
46-
# Allows the user to change the ID, which forces them back through GATE 1
47-
if st.button("Change Prolific ID", help="Clicking this will clear your stored ID and return you to the ID entry form."):
48-
config_set("prolific_id", "") # Clear the stored ID
49-
st.rerun()
50-
72+
# Check if the warning is NOT suppressed. If it's not suppressed, we show the UI
73+
# and MUST return True (Block execution) until the user clicks the button.
5174
if not config_get("suppress_warning", False):
5275
st.markdown("---")
5376
st.subheader("2. Network Monitoring Warning")
5477
st.markdown(warning_text)
5578

56-
if st.button("OK, I understand and wish to proceed", help="Clicking this confirms that you understand the warning and wish to proceed."):
57-
config_set("suppress_warning", True)
58-
st.rerun()
79+
st.button("OK, I understand and wish to proceed",
80+
on_click=remove_warning,
81+
help="Clicking this confirms that you understand the warning and wish to proceed.")
82+
83+
# Since the warning is displayed and unaccepted, we must block.
84+
return True
5985

60-
return not is_prolific_id_valid(config_get("prolific_id", ""))
86+
# If we reach here, ID is valid AND suppress_warning is True.
87+
return False
6188
else:
6289
# ID is missing or invalid -> BLOCK and show input form
6390
st.subheader("Prolific ID Required")
@@ -69,15 +96,10 @@ def show_warning():
6996
value="",
7097
key="prolific_id_input"
7198
).strip()
72-
73-
submitted = st.form_submit_button("Submit ID")
74-
if submitted:
75-
if is_prolific_id_valid(input_id):
76-
config_set("prolific_id", input_id)
77-
st.success("Prolific ID accepted. Please review the details below.")
78-
st.rerun() # Rerun to jump to the confirmation step (GATE 2)
79-
else:
80-
st.error("Invalid Prolific ID. Must be 1-50 alphanumeric characters.")
99+
st.form_submit_button("Submit ID",
100+
on_click=set_prolific_id,
101+
args=(input_id,),
102+
help="Submit your Prolific ID to proceed.")
81103

82104
return True # BLOCK: ID check still needs resolution.
83105

@@ -157,6 +179,7 @@ def plot_traffic_volume(df: pd.DataFrame, now: int, chart_title: str):
157179
df_reindexed = df_reindexed.sort_values(by='seconds_ago', ascending=False)
158180
st.bar_chart(df_reindexed.set_index('seconds_ago')['Bits'], width='content')
159181

182+
160183
def get_device_metadata(mac_address: str) -> dict:
161184
"""
162185
Retrieve the DHCP hostname and OUI vendor for a device from the database.

src/libinspector/device_detail_page.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,6 @@ def process_network_flows(df: pandas.DataFrame):
430430
local_timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
431431
df['first_seen'] = df['first_seen'].dt.tz_localize('UTC').dt.tz_convert(local_timezone)
432432
df['last_seen'] = df['last_seen'].dt.tz_localize('UTC').dt.tz_convert(local_timezone)
433-
434-
df['inferred_activity'] = None
435-
df['confirmation'] = False
436433
df = df.reset_index(drop=True)
437434

438435
st.markdown("#### Network Flows")
@@ -477,7 +474,7 @@ def show_device_details(mac_address: str):
477474
SELECT MIN(timestamp) AS first_seen,
478475
MAX(timestamp) AS last_seen,
479476
COALESCE(dest_hostname, dest_ip_address) AS dest_info,
480-
SUM(byte_count) * 8 AS Bits
477+
ROUND(SUM(byte_count) / 1024.0, 2) AS KiloBytes
481478
FROM network_flows
482479
WHERE src_mac_address = ?
483480
AND timestamp >= ?
@@ -490,7 +487,7 @@ def show_device_details(mac_address: str):
490487
SELECT MIN(timestamp) AS first_seen,
491488
MAX(timestamp) AS last_seen,
492489
COALESCE(src_hostname, src_ip_address) AS src_info,
493-
SUM(byte_count) * 8 AS Bits
490+
ROUND(SUM(byte_count) / 1024.0, 2) AS KiloBytes
494491
FROM network_flows
495492
WHERE dest_mac_address = ?
496493
AND timestamp >= ?

src/libinspector/device_list_page.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,24 @@ def worker_thread():
4343
for device_dict in device_list:
4444
meta_data = common.get_device_metadata(device_dict['mac_address'])
4545
remote_hostnames = common.get_remote_hostnames(device_dict['mac_address'])
46+
custom_name_key = f"device_custom_name_{device_dict['mac_address']}"
4647
try:
4748
# Note I am passing the metadata as a string because functions with cache cannot take dicts
4849
# as a dict is mutable, and the cache would not work as expected.
4950
api_output = call_predict_api(json.dumps(meta_data), remote_hostnames, device_dict['mac_address'])
5051
common.config_set(f'device_details@{device_dict["mac_address"]}', api_output)
5152
if "Vendor" in api_output:
52-
# Update name based on API
53-
custom_name_key = f"device_custom_name_{device_dict['mac_address']}"
5453
custom_name = api_output["Vendor"]
5554
if api_output["Vendor"] != "":
5655
common.config_set(custom_name_key, custom_name)
57-
else:
58-
# If API is down, just try using OUI vendor
59-
common.config_set(custom_name_key, meta_data.get('oui_vendor', 'Unknown Device, likely a Mobile Phone'))
6056
except Exception as e:
6157
logger.info("[Device ID API] Exception when calling API: %s", str(e))
62-
continue
63-
64-
58+
finally:
59+
# If API is down, just try using OUI vendor if no custom name is set in config.json
60+
if common.config_get(custom_name_key, default='') == '':
61+
common.config_set(custom_name_key,
62+
meta_data.get('oui_vendor', 'Unknown Device, likely a Mobile Phone'))
63+
6564
@functools.cache
6665
def call_predict_api(meta_data_string: str, remote_hostnames: str,
6766
mac_address: str, url="https://dev-id-1.tailcedbd.ts.net/predict") -> dict:

src/libinspector/page_manager.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ def _show_page_func_wrapper():
2828
st.query_params['pid'] = pid
2929

3030
initialize_page()
31-
3231
st.markdown(f"## {icon} {title}")
33-
3432
show_page_func()
3533

3634
return st.Page(
@@ -52,7 +50,7 @@ def initialize_page():
5250
menu_items={}
5351
)
5452

55-
# If true, block further execution
53+
initialize_config()
5654
if common.show_warning():
5755
st.stop()
5856

@@ -67,15 +65,18 @@ def initialize_page():
6765
start_inspector_once()
6866

6967

68+
@functools.lru_cache(maxsize=1)
69+
def initialize_config():
70+
"""Initialize certain Config variables when starting IoT Inspector."""
71+
common.config_set("suppress_warning", False)
72+
common.config_set("labeling_in_progress", False)
73+
common.config_set("api_message", "")
74+
75+
7076
@functools.lru_cache(maxsize=1)
7177
def start_inspector_once():
7278
"""Initialize the Inspector core only once."""
7379
with st.spinner("Starting Inspector Core Library..."):
74-
# Just in case someone closes labeling window without finishing
75-
# Same with the general warning
76-
common.config_set("suppress_warning", False)
77-
common.config_set("labeling_in_progress", False)
78-
common.config_set("api_message", "")
7980
libinspector.core.start_threads()
8081
api_thread = threading.Thread(
8182
name="Device API Thread",

src/libinspector/server/packet_collector.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,14 +185,17 @@ def make_pcap_filename(start_time: int, end_time: int) -> str:
185185
Returns:
186186
str: Human-readable filename.
187187
"""
188-
start_dt = datetime.datetime.fromtimestamp(start_time)
189-
duration_seconds = end_time - start_time
188+
# Determine the local timezone object of the machine running the script.
189+
local_tz = datetime.datetime.now().astimezone().tzinfo
190190

191-
# Format the start time: e.g., 'Oct-31-2025_113100AM'
192-
safe_start = start_dt.strftime("%b-%d-%Y_%I:%M:%S%p")
191+
# We explicitly tell fromtimestamp() that the input is UTC.
192+
start_dt_utc = datetime.datetime.fromtimestamp(start_time, tz=datetime.timezone.utc)
193+
start_dt_localized = start_dt_utc.astimezone(local_tz)
194+
duration_seconds = end_time - start_time
195+
safe_start = start_dt_localized.strftime("%b-%d-%Y_%I:%M:%S%p")
193196

194197
# Generate the filename with duration
195-
filename = f"{safe_start}_{duration_seconds}s.pcap"
198+
filename = f"{safe_start}_{duration_seconds:.2f}s.pcap"
196199
return filename
197200

198201

0 commit comments

Comments
 (0)