GOOGLE SUMMER OF CODE 2026: FINAL WORK PRODUCT CERT-Polska/Artemis

Extending the Artemis Scanner

Student
Rafli Permana
Mentor
Krzysztof Zając
Organization
The Honeynet Project
Project
01

Overview

I spent this summer as a GSoC (Google Summer of Code) contributor on Artemis, a modular vulnerability scanner, under the mentorship of Krzysztof Zając. Artemis checks multiple aspects of website security and builds easy-to-read messages to send to organizations to get the vulnerabilities fixed - it’s the tool CERT Polska (Computer Emergency Response Team Poland) has used to find and report almost two million vulnerabilities and misconfigurations since 2023. I built a crawling pipeline and a frontend auth layer, added direct URL scanning, put CPE/CVE lookups on detected technologies, and more.

Artemis runs on Python 3.13 with Karton as the distributed task queue, Docker for the scanner modules, Redis for queueing and caching, and PostgreSQL for storing results. Every scanner module subclasses ArtemisBase, declares which Karton tasks it wants via filters, and saves findings that a separate reporting layer turns into HTML and email reports for the organization being scanned.

02

Before GSoC

I opened my first Pull Request on Feb 22, 2026 - about two months before I was accepted. I didn’t know the codebase at all, so I started small: layout fixes, a redirect status code that was technically wrong (M2363), templates that hardcoded paths instead of using request.url_for (M2368). Some infra bugs too - an IPv6 crash in the blocklist range check (M2596), a flaky Grafana healthcheck (M2526).

Initially Artemis’s injection detectors would report every parameter they’d injected, even when only one of them was actually vulnerable - so a report might list five parameters when only one mattered. I added parameter minimization: after a hit, re-test each parameter individually and keep only the ones that independently reproduce the issue. Shipped it for LFI first (M2423, which also fixed a batching bug, capped further in M2525), then for SQL injection parameters (M2483) and SQL injection headers (M2584).

By the time GSoC officially started I had landed 15 merged PRs. show all ▾
  • M2335Remove -recursive flag from subfinder and add integration testFeb 23, 2026
  • M2362Fix layout shift caused by conditional vertical scrollbarFeb 26, 2026
  • M2363Replace 301 with 303 for redirects following POST requestsFeb 26, 2026
  • M2368Refactor templates to consistently use request.url_for instead of
    hardcoded paths
    Mar 3, 2026
  • M2413Replace confirmation redirect pages with modal pop upsMar 4, 2026
  • M2420Add support for configuring Subfinder providers via environment variableMar 10, 2026
  • M2423Add parameter minimization to LFIDetector and fix batching bugMar 10, 2026
  • M2484Add Artemis-modules-extra folder to .gitignoreMar 13, 2026
  • M2525Add LFI parameter minimization cap and corresponding testMar 19, 2026
  • M2526Increase Grafana healthcheck start_period to 60sMar 19, 2026
  • M2483Implement parameter minimization to SqlInjectionDetectorMar 19, 2026
  • M2473Add redirect detection for successful login attempts in admin_panel_login_bruter moduleMar 24, 2026
  • M2584Implement header minimization for SQL injection detectionApr 15, 2026
  • M2596Fix IPv6 crash in blocklist IP range checksApr 13, 2026
  • M2741Drop badkeys --update-bl from docker buildMay 18, 2026
03

The proposal

The project was split into two coding periods. Period 1 was infrastructure: direct URL scanning, a real crawling pipeline, a frontend authentication layer, and making Wappalyzer’s technology detection actually usable by downstream modules instead of just sitting in Postgres. Period 2 was supposed to build three new vulnerability detectors on top of that foundation - SSTI, OS command injection, and NoSQL injection.

The ordering wasn’t arbitrary. The crawl pipeline had to exist before any detector could use it, and technology tags had to flow through the task queue before anything could route on them. Build the plumbing first, then the things that use the plumbing.

04

What I built

04.1 - deliverablePeriod 1 / merged

Frontend authorization layer

Every frontend route in Artemis was wide open. The only protection was CSRF on POST requests, which stops forged requests but does nothing if someone just browses to [ip]:5000 and reads your scan results directly. M2679 added a login page, session auth, and a FastAPI dependency on every frontend route.

I took this one first because it was a deployment blocker rather than a feature. Everything else on the roadmap puts more findings into that dashboard, so the longer an unauthenticated frontend stayed in place, the more there was behind it to walk into.

04.2 - DELIVERABLEPERIOD 1 / MERGED

Crawling pipeline

This is the dependency the whole Period 2 plan sat on. A detector can only test the URLs it gets handed, so whatever coverage the crawler produced was the ceiling on every injection module I was going to write later - which is why it had to be rebuilt before any of them.

Artemis used to do single-page HTML parsing for crawling - no JS execution, no following links, no dedup. M2738 replaced that with Katana (run with -jc so it picks up endpoints buried in JS files) piped through uro for structural deduplication, cached in Redis behind a distributed lock so ten modules scanning the same host trigger exactly one crawl instead of ten, with a shorter cache TTL when the crawl times out so partial results don’t stick around for a full day. Four consumer modules migrated onto it in the same PR.

My original proposal had a third stage - gf for vulnerability-class filtering - and I dropped it. gf filters URLs by matching parameter names against English-language wordlists, but Artemis scans a lot of Polish infrastructure, where parameter names don’t reliably match those wordlists. A genuinely vulnerable parameter with an unusual name could get silently filtered out before any detector ever saw it - an optimization that can hide a real vulnerability isn’t a safe default.

04.3 - DELIVERABLEPeriod 1 / merged

Direct URL scanning

Artemis works out what to do with a target by classifying whatever you hand it, and each class drops into a fixed pipeline of modules. Direct URL scanning just means being able to hand it one exact address and have that address scanned as-is. I pulled it forward because it shortens the loop for everyone - my own testing for the rest of the project, and anyone who already knows the single endpoint they want checked.

Before this, Artemis’s classifier only understood domains, IPs, IP ranges, and ASNs. If we submitted a domain, it went through subdomain enumeration, then port scanning, then fingerprinting before any HTTP module ever saw a task - even if we already knew the exact URL we wanted scanned. M2899 taught the classifier to parse URLs directly and emit the TaskType.SERVICE task without the detour, so every HTTP module picks it up with zero changes - it’s the same task type the port scanner would eventually have produced anyway.

04.4 - DELIVERABLEPeriod 1 / merged

CPE tagging for detected technologies

This started small. Wappalyzer - the fingerprinting library Artemis uses to work out what software a site is running - detects things like Django or MongoDB on a target, but that result only went into Postgres for the report, so no downstream module could ever see it. M2978, fixed that: structured Technology objects carrying CPE (Common Platform Enumeration), version, and categories on the task payload, so downstream modules can route on what’s running on a target instead of reading it back out of the report. A CPE is the standardized name for one specific product at one specific version, and it’s the key vulnerability databases are indexed by - without it, “MongoDB 4.2” is just a string.

This is the tagging work only, with no vulnerability lookup yet, and splitting it out was deliberate. Same pattern as the crawler: the tags had to actually reach downstream modules through the task queue before anything could use them, so this had to land first - without it, the CVE lookup below would have no reliable way to know what product or version it was even checking.

04.5 - DELIVERABLEPeriod 1 / merged

CVE lookup for detected technologies

A CVE (Common Vulnerabilities and Exposures) is the public identifier for one specific known flaw in one specific piece of software, and it’s what makes the CPE strings from the previous deliverable worth carrying around. Building on that tagging from M2978, M2842 is an actual CVE discovery flow: switch the Go Wappalyzer wrapper to FingerprintWithInfo to get CPE strings, then query NVD (National Vulnerability Database) for known CVEs against them.

This is the payoff the tagging work was for, and it’s cheap in exactly the way the rest of the scanner isn’t: the target has already been fingerprinted, so turning “this host runs X 1.2” into “this host runs a version with known reported flaws” is a database question, not more probing traffic aimed at someone’s server.

04.6 - deliverablePERIOD 2 / MERGED

OS command injection detector

With the crawler and the tagging in place, Period 2 was the detectors. I started with this one because it has the least ambiguous proof of the three: a command either executed on the target or it didn’t, so I could get the shared scaffolding right against a signal I trusted before moving to detectors that have to infer a result from error text.

M2947 implements two detection methods. Output-based: inject an echo of a random per-request marker through each shell separator (;, |, newline, $(...), backticks) and check whether the marker comes back - that proves execution actually happened, not just that the app returned a suspicious error string. Time-based blind: inject sleep N through the same separators and confirm a reproducible delay across several rounds measured against a baseline, for cases where there’s no visible output to check.

Since merging, the detector has already returned a real finding during production scanning - confirmation that the two detection methods hold up against live targets, not just the test suite.

04.7 - DELIVERABLEPERIOD 2 / MERGED

Extracting shared logic

M3028 removed code instead of adding it. The SQL, ORM, and LFI detectors each ran the same sequence at the top of their run() method - crawl the target, add the seed URL back in, union in query-stripped variants, filter out static assets, cap the result - and the three copies had already started drifting apart from each other. I moved it into one function, get_links_to_scan(), in crawling.py, and all three detectors call it instead of carrying their own copy.

A further piece of sharing - a common result-building helper in injection_helpers.py and crawling.collect_parameters - landed inside O3051 (the NoSQL injection PR) instead of its own PR.

LEDGER - 27 PULL REQUESTS TO CERT-POLSKA/ARTEMIS
M25 merged O1 open C1 closed, replaced by a corrected reopen
~5,403 / 457
lines added / removed in merged code
~1,951
more lines sitting in the open PR
~7,811
total lines touched across merged and open PRs
What I delivered in Artemis during GSoC - 11 delivered, 1 in review
  • M2679Implement authorization layer for frontend routesMay 10, 2026
  • M2738Implement Katana + uro crawling pipeline with shared Redis cacheMay 22, 2026
  • M2496Implement ORM injection detector moduleJun 15, 2026
  • M2883Fix false positives from flaky services and save the probe URLsJun 28, 2026
  • M2844Build all Go tools with one Go 1.25 toolchainJul 2, 2026
  • M2899Implement direct URL scanningJul 8, 2026
  • M2978Add CPE information to detected web technologiesJul 30, 2026
  • M3028Extract shared crawl helpers from injection detectorsAug 12, 2026
  • M2947Implement OS command injection detector moduleAug 19, 2026
  • M2842Add CVE lookup for detected web technologiesAug 25, 2026
  • O3051NoSQL injection detectoropened Aug 18, 2026
05

What’s still open / what’s next

O3051, NoSQL injection detector. Error-based detection with MongoDB operator payloads against both GET params and JSON bodies, matched against a clean baseline so normal error messages don't get flagged as vulnerable, plus blind detection for endpoints that swallow their errors and never surface a visible difference.

It had its first round of review and is now awaiting the follow-up changes. It also introduces injection_helpers.py, a new shared helper file for result-building and deduplication logic that used to be duplicated across detectors, plus a collect_parameters() helper in crawling.py.

06

What I learned

A scanner that reports false positives is worse than one that reports nothing at all, because someone on the other end has to hand-verify every single finding before it goes out to an organization.

Shipping a detector or a feature is maybe 30% of the work. The reporter, the tests, the Docker integration, and the edge cases around it are the other 70%, and skipping any of them means it isn't actually done.

07

Thanks

Thanks to Krzysztof Zając for mentoring me through this, for the design pushback that made the final result better than what I would've shipped on my own. Thanks to Krzysztof Waliczek, who put in a lot of careful review time across the summer and caught things I would've missed. And I'm grateful to The Honeynet Project and CERT Polska for giving me the opportunity in the first place.