HW Blog

1. SF Radar Map 2. RMWCCDC Regional Qualifiers 3. Cron Misconfigurations: Graffiti Tiles

1. SF Radar Map

December 13, 2025

This radar map ingests data from two SF city government Socrata databases, fire and service calls, which update records of all open and closed service calls within the city limits every 10-20 minutes.

I created a cloudflare worker to handle API calls, and simply call it upon page load.

The SF APIs respond with a GeoJSON FeatureCollection containing a Feature for each incident. there's a lot of interesting data attached to these Features that simply does not map well or add to incident context, so I recommend checking out all of the data fields.

Each incident is parsed and mapped using Leaflet.js, a comprehensive library for easy construction of maps. The layer feature of this library is what I used to parse an incidents data into a Leaflet.js pointToLayer. Leaflet.js also uses tile-caching to keep performance speedy and supports web raster basemaps like the one i used from CARTO (they do a good job of trying to make you pay, but their free API access is still around).

Some calls are marked as sensitive and will not include a location. Generally, non-sensitive calls will have a cross-street as a location.

Severity is determined by the priority rating given to each call by the dispatchers, and will update if a call has been reassessed.

I'm currently in the process of integrating a grouping feature for calls that occur in the same area and are possibly connected. For example, multiple accounts of shots fired within a five-block radius are likely connected to a single incident and should be indicated as such.

2. RMWCCDC Regional Qualifiers

January 26, 2026

In late January of 2026, my team at the University of Denver participated in the Rocky Mountain Collegiate Cyber Defense Competition (RMCCDC) qualifier. The RMCCDC is a regional round of the National Competition (NCCDC), a realistic and respected cyber defense competition in the U.S.

This was my first time participating in a defense competition, and I was particularly excited for the steep learning curve that comes from defending an enterprise network against professional red-teamers. Apart from service uptime and defense, the competition is also judged on successful injects. Think of these as technical business requests that the team must fulfill without violating an SLA (usually within 15-30 minutes).

Our team consisted of six undergraduate CS students and one master's cybersecurity student.

The tournament is judged using the following schema:
35-50%: functional services uptime,
35-50%: successful completion of inject scenarios,
10-20%: exploitation & incident response.

I was initially feeling a bit uneasy going into this tournament. I had heard that the first hour was very demanding, as we would be setting up firewalls and resetting credentials on all of the systems. I was right to be uneasy, but in reality, this part of the competition allowed us to learn the components of our services and which traffic we'd need to create custom rules for.

Thanks to the memo templates we had developed before flag drop, injects were not as difficult as initially estimated. For each inject, we delegated tasks based on experience, interest, and pre-decided team roles. I worked on a lot of firewall logging injects: I looked for malicious/unauthorized communications, configured traffic rules at each network layer, and investigated outages when they inevitably occurred.

Communication was a key factor in our success, and despite no prior NCCDC experience across our team, we did a great job at maintaining clarity and composure. Verifying credentials and making sure everyone knew where their purpose lied on each inject/outage was crucial to staying ahead of the attack curve.

I thought that the red team did a pretty good job, and was honestly a bit surprised we didn't get hit as hard as some of the other teams. One of the main lessons that we learned in the qualifier was to immediately check for patches on our firewalls, OS, and services. One of our eight services kept going dark, and we observed unauthorized changes to our services throughout the majority of the competition on said service. I can only presume that an early patch would have been a great step in increasing uptime on that particular service. It was a long, strung-out day, but extremely fun.

We have a lot to review and learn before the regional, which will be held on March 6th/7th at Regis University, and I am super excited!

3. Cron Misconfigurations: Graffiti Tiles

June 15, 2026

With the end of the school year and a lack of assignments, I'll hopefully have some more time to write about my projects. I'll start with one of the projects on my home page: Graffiti Tiles. In this project, I developed a site that automatically displays the last 24 hours of graffiti 311 cases in San Francisco. I used the same infrastructure as my 911 call map project to support this website. The main platform that I used to accomplish my goal was Cloudflare Workers. Similar to the 911 call project, I created a worker to make fetches to a Socrata database, which the San Francisco Data Team updates every day with new 311 cases across many categories.

Some of the categories that this database contains are quite interesting, while others contain little to no interesting data. Many of the 311 calls are related to street closures or construction projects, which I think could be interesting subjects for a future project using other data sources.

So let's get down to the technical challenges of this project. Firstly, from the reliability and optimization side, I didn't want to create unnecessary calls to the Socrata API and make the SF data people mad at me, so the purpose of the worker was to create a pseudo-API with a /data and /refresh endpoint. The /data endpoint serves my data, and the /refresh endpoint actually queries the database with the crafted input:
https://data.sfgov.org/resource/vw6y-z8j6.geojson?$select=*&$where=requested_datetime >= '{datetime}T00:00:00' AND service_name IN('Graffiti Private','Graffiti Public','Graffiti') AND media_url IS NOT NULL&$order=requested_datetime DESC&$limit=1000

One of the challenges that comes from pulling data from a city-managed database is that I have no control over what CDN they use. In the case of 311 service tickets, they use two different CDNS: Cloudinary, and Verint Cloud Services. On Cloudinary, the ticket attachments come in the form of full URL .jpeg images. On verint, the images are of the type data:image/jpeg;base64. Unfortunately, there is no reliable way to parse this type of image through a worker, so that's an unsolved problem.

Cloudflare also helps us automate our /refresh hits, by making cron jobs easily configurable. Using wrangler, I was able to set a cron job to populate my KV data every morning at 2 AM MT. In hindsight, this was a costly mistake that I made in my first iteration of the project. After a day of smooth data feeding into my site with manual calls to /refresh, I noticed that my scheduled cron jobs were resolving with 200 codes on Cloudflare's side, but I wasn't seeing any data on my /data endpoint or my frontend. After looking at my logs in wrangler for this worker and the one I used in my 911 radar project, I confirmed that this issue was not related to any Socrata or Cloudflare firewalling.

I realized that I had missed one check that would confirm if this was a cron issue or not: actually looking at the Socrata endpoint manually, without piping it through my own worker. I tried one of the endpoints that my cron job had just queried with no data, and honed my analysis in on the timestamp field. My cron job was fetching data from the past 24 hours at 2 AM UTC. When I looked at the Socrata database entry changelog, I noticed that updates were occurring at around 4 AM MT, with a few minutes of deviation. So that was the root cause of my issue-- my worker was not getting any data because it was querying before the data was updated.

I updated the cron job to follow the expression 0 12 * * * and voila, the following day I started pulling data automatically. Additionally, I offset my cron schedule by one hour to account for possible deviations due to daylight savings, as SF is in the PDT timezone for part of the year. This project taught me about cron scheduling, specifically troubleshooting cron misconfigurations and mismatches between what I can control and what is out of my hands.