afterhours-shift-manager/src/roster-sync/app.py
Adam Moussa ae4e2740a8
fix(logging): remove taint-flagged values from 3CX and roster-sync logs
Resolves code-scanning alerts 12-15 (py/clear-text-logging-sensitive-data).
CodeQL taints the 3CX response dicts via the Secrets Manager-sourced
domain in the request URL, so entity IDs subscripted from those
responses (ivr_id, resource_id, queue_id) and the roster result dict
trip the query. None of the flagged values are secrets, but the log
lines are rewritten so the pattern cannot trip: entity IDs are dropped
in favor of the untainted destination DNs, and the roster summary logs
counts instead of the member-derived dict (which also keeps employee
names out of the logs).
2026-07-27 13:31:01 -04:00

130 lines
4.1 KiB
Python

"""Lambda handler — syncs the DynamoDB roster from 3CX group members.
Runs daily via EventBridge. Pulls members from the configured 3CX group,
filters to Extension type only (excludes RingGroups, IVRs, Voicemail, etc.),
and syncs to DynamoDB. Preserves existing slack_user_id links.
"""
import logging
import os
from datetime import datetime
from zoneinfo import ZoneInfo
from shared.three_cx_client import ThreeCXClient
from shared.schedule import ShiftSchedule
from shared.secrets import get_secret
logger = logging.getLogger()
logger.setLevel(logging.INFO)
EASTERN = ZoneInfo("America/New_York")
EXCLUDE_NAMES = {"Voicemail", "IVR", "Fax"}
def handler(event, context):
now = datetime.now(EASTERN)
# DST guard — two EventBridge rules fire, only one is at 6am ET
force = event.get("force", False)
if not force and now.hour != 6:
logger.info(
"ET hour is %d, not 6am — skipping (wrong DST rule fired)", now.hour
)
return {"skipped": True}
group_name = os.environ.get("SYNC_GROUP", "DEFAULT")
logger.info(
"Starting roster sync from 3CX group '%s' at %s", group_name, now.isoformat()
)
secret_prefix = os.environ["TCX_SECRET_PREFIX"]
client = ThreeCXClient(
domain=get_secret(f"{secret_prefix}domain"),
auth_mode="oauth",
client_id=get_secret(f"{secret_prefix}client-id"),
client_secret=get_secret(f"{secret_prefix}client-secret"),
)
members = client.get_group_members(group_name)
# Only sync actual phone extensions, not ring groups, IVRs, etc.
extensions = [
m
for m in members
if m.get("Type") == "Extension" and m.get("MemberName") not in EXCLUDE_NAMES
]
logger.info(
"Found %d extensions in 3CX group '%s' (filtered from %d members)",
len(extensions),
group_name,
len(members),
)
schedule = ShiftSchedule()
current_roster = {item["SK"]: item for item in schedule.get_roster()}
threecx_extensions = set()
added = []
updated = []
for ext in extensions:
number = str(ext.get("Number", ""))
name = ext.get("MemberName", "").strip()
if not number or not name:
continue
threecx_extensions.add(number)
existing = current_roster.get(number)
if existing:
if existing.get("name") != name:
schedule.table.update_item(
Key={"PK": "ROSTER", "SK": number},
UpdateExpression="SET #n = :name",
ExpressionAttributeNames={"#n": "name"},
ExpressionAttributeValues={":name": name},
)
updated.append(f"Ext {number}: {existing['name']} -> {name}")
else:
schedule.table.put_item(
Item={
"PK": "ROSTER",
"SK": number,
"name": name,
"extension": number,
"slack_user_id": "",
}
)
added.append(f"Ext {number}: {name}")
removed = []
# Only remove departed employees if we got results from 3CX
# (prevents wiping roster on API errors or misconfigured group)
if threecx_extensions:
for ext_number, item in current_roster.items():
if ext_number not in threecx_extensions:
schedule.table.delete_item(Key={"PK": "ROSTER", "SK": ext_number})
removed.append(f"Ext {ext_number}: {item.get('name', 'Unknown')}")
elif current_roster:
logger.warning(
"No 3CX extensions found but roster is non-empty — skipping removal to prevent data loss"
)
result = {
"group": group_name,
"total_3cx": len(threecx_extensions),
"added": added,
"updated": updated,
"removed": removed,
}
logger.info(
"Roster sync complete: group=%s total_3cx=%d added=%d updated=%d removed=%d",
group_name,
len(threecx_extensions),
len(added),
len(updated),
len(removed),
)
return result