Skip to content

feat(phishing): add fully native phishing injector (#319) - #329

Open
SamuelHassine wants to merge 7 commits into
mainfrom
feat/injector-phishing
Open

feat(phishing): add fully native phishing injector (#319)#329
SamuelHassine wants to merge 7 commits into
mainfrom
feat/injector-phishing

Conversation

@SamuelHassine

@SamuelHassine SamuelHassine commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

  • Add the fully native phishing injector: an embedded SMTP sender plus a stdlib tracking web server (open pixel, click landing page, credential submission), requiring no external phishing platform.
  • Wire CI: add phishing to the CircleCI build_docker_images, publish_images and test matrices.

Notes

  • Submitted credentials are never stored - only the fact of submission.
  • Requires a publicly reachable tracking URL (reverse proxy + TLS in prod); token to recipient mapping is in-memory for the campaign window.
  • Complements the Gophish wrapper injector.
  • A placeholder square 512x512 icon (solid opaque background) is included so the injector starts; the final generic phishing/security mark will be set per Connector icons should be square with a solid background (netexec, teams, ...) #305.

Review hardening (applied in this PR)

  • Record open/click/submit under the store lock so counts stay consistent with stats() on the threaded tracking server.
  • Validate and cap the submission Content-Length (return 413) so a malformed or oversized header cannot 500 or tie up a handler thread.
  • Set a per-request socket timeout on the tracking handler so a client that advertises a small Content-Length and then slow-sends or stalls the body cannot pin a handler thread indefinitely (slowloris), which would otherwise defeat the size cap.
  • Close the connection on a rejected submit (413) so the undrained body cannot desynchronize a keep-alive client, and catch the timed body read so a stalled client hits the socket timeout and closes cleanly instead of bubbling a traceback out of BaseHTTPRequestHandler.
  • Send no-store cache headers on the open pixel so repeated opens are not undercounted by caches.
  • Join the tracking server thread on stop() for deterministic shutdown, and guard httpd.shutdown() behind the started-thread check so stop() before start() cannot deadlock.
  • Resolve the icon path relative to the module and ship a placeholder icon, removing the startup FileNotFoundError.
  • execution_output_structured is now a single json.dumps of a plain object (sent, total, tracking_url) - no nested JSON strings; the always-zero send-time stats were dropped since open/click/submit are scored via the contract's manual "Human Response" expectation.
  • Surface per-recipient send failures on a partial success (the launch stays SUCCESS but failed recipients are appended to the execution message) instead of dropping them.
  • Recipients field label now reads "comma, semicolon or newline separated", matching parse_recipients().
  • Added unit tests for the SMTP error path, the partial-failure message, stop() before start(), the request-timeout guard, and the oversized-Content-Length rejection (413 + connection close).

Test plan

  • isort/black/flake8/ruff clean
  • python -m unittest (contracts, templates, live tracking-server HTTP test, oversized-body 413/connection-close test, SMTP sender success/connection/SMTP error, request-timeout guard) - 22 tests pass
  • docker build with --build-context injector_common (CircleCI build_docker_images-phishing green)

Follow-ups

Closes #319

Self-contained phishing injector with an embedded SMTP sender and stdlib tracking web server (open pixel, click landing page, credential submission), requiring no external phishing platform. Wires the injector into the CircleCI build, publish and test matrices.
Copilot AI review requested due to automatic review settings July 14, 2026 09:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new phishing/ injector that runs fully natively (SMTP sending + embedded stdlib tracking server) and wires it into the existing CircleCI test/build/publish matrices so it’s built and tested alongside the other injectors.

Changes:

  • Added a new native phishing injector package with configuration, contracts, SMTP sender, template rendering, and an embedded tracking HTTP server.
  • Added an initial unittest suite covering contracts, template rendering, recipient parsing, and live tracking endpoints.
  • Updated CircleCI matrices to include the new phishing injector in test/build/publish jobs.

Reviewed changes

Copilot reviewed 21 out of 25 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
phishing/test/test_phishing.py Adds unit/integration tests for contracts/templates/parsing and the embedded tracking server.
phishing/test/init.py Marks the phishing test directory as a package.
phishing/README.md Documents the injector’s purpose, constraints, and development workflow.
phishing/pyproject.toml Defines project metadata, dependencies, dev tooling, and icon path metadata.
phishing/phishing_injector/tracking/store.py Implements an in-memory campaign store for token→recipient mapping and event stats.
phishing/phishing_injector/tracking/server.py Implements the embedded tracking HTTP server (open pixel, click landing, submit redirect).
phishing/phishing_injector/tracking/init.py Marks the tracking module as a package.
phishing/phishing_injector/openaev_phishing.py Main injector entrypoint: config loading, SMTP sending loop, tracking server startup, callback reporting.
phishing/phishing_injector/img/README.md Describes required icon asset and icon standard constraints.
phishing/phishing_injector/helpers/templates.py Adds built-in phishing templates and rendering with per-recipient tracking URLs.
phishing/phishing_injector/helpers/phishing_sender.py Adds SMTP sender helper (stdlib smtplib + email.message).
phishing/phishing_injector/helpers/init.py Marks helpers as a package.
phishing/phishing_injector/contracts_phishing.py Defines the phishing contract (fields + manual “Human Response” expectation).
phishing/phishing_injector/configuration/phishing_config.py Adds pydantic settings model for tracking + SMTP configuration.
phishing/phishing_injector/configuration/injector_config_override.py Defines injector-level overrides (id/name/icon path).
phishing/phishing_injector/configuration/config_loader.py Wires configuration into the OpenAEV daemon config hints and injects contracts.
phishing/phishing_injector/configuration/init.py Marks configuration as a package.
phishing/phishing_injector/init.py Adds package marker and a __version__.
phishing/manifest-metadata.json Adds integration catalog metadata for the phishing injector container.
phishing/Dockerfile Adds multi-stage build/run image for the phishing injector.
phishing/docker-compose.yml Adds a sample compose service for local runs.
phishing/config.yml.sample Adds sample YAML configuration for OpenAEV + injector + phishing settings.
phishing/.env.sample Adds sample environment variable configuration.
phishing/.dockerignore Adds dockerignore rules for the phishing injector build context.
.circleci/config.yml Adds phishing to the CircleCI test/build/publish matrices.

Comment thread phishing/phishing_injector/tracking/store.py Outdated
Comment thread phishing/phishing_injector/tracking/store.py Outdated
Comment thread phishing/phishing_injector/tracking/store.py Outdated
Comment thread phishing/phishing_injector/tracking/server.py
Comment thread phishing/phishing_injector/tracking/server.py Outdated
Comment thread phishing/phishing_injector/tracking/server.py
Comment thread phishing/phishing_injector/openaev_phishing.py
Comment thread phishing/phishing_injector/openaev_phishing.py
Comment thread phishing/phishing_injector/openaev_phishing.py Outdated
Comment thread phishing/phishing_injector/helpers/phishing_sender.py
Adds coverage for __init__, process_message campaign flow, start and the SMTP sender to satisfy the codecov patch gate.
Address review findings on the native phishing injector:

- Add a placeholder 512x512 icon-phishing.png (solid opaque background) and
  resolve ICON_PATH relative to the module so the injector no longer crashes
  with FileNotFoundError at startup. Final mark to be set in #305.
- Record open/click/submit under the store lock to avoid a data race with
  stats() on the threaded tracking server.
- Validate and cap the submission Content-Length (return 413) so a malformed
  or oversized header cannot 500 or tie up a handler thread.
- Send no-store cache headers on the open pixel to avoid undercounting opens.
- Join the tracking server thread on stop() for deterministic shutdown.
- Stop double-encoding execution_output_structured and drop the always-zero
  send-time stats; the launch callback now reports sent/total and the tracking
  URL, and the README explains human response is scored via the manual
  expectation.
- Add a unit test for the SMTP error path in PhishingSender.send.
@SamuelHassine

Copy link
Copy Markdown
Member Author

Review and fix summary

Did an independent senior-reviewer pass over the full changed files (not just the diff) and applied all fixes in 36b0215 (signed). All 10 automated review threads are resolved and CI is fully green.

Fixes applied

  • Missing icon (blocking): the injector opened img/icon-phishing.png unconditionally but the file was absent, so it would raise FileNotFoundError at startup. Added a placeholder square 512x512 PNG (solid opaque background) and resolve the path relative to the module. Final mark to be set per Connector icons should be square with a solid background (netexec, teams, ...) #305.
  • Structured output (blocking): execution_output_structured was double-encoded (JSON strings inside a JSON object, scalars wrapped in single-element lists) and reported always-zero stats computed at send time. It now emits a single json.dumps of a plain object (sent, total, tracking_url); open/click/submit are scored via the contract's manual "Human Response" expectation.
  • Store thread-safety: record_open/click/submit mutated the Recipient outside the lock, racing with stats() on the threaded server. Mutations now happen under the store lock.
  • Submission endpoint: do_POST parsed Content-Length blindly (malformed -> 500, oversized -> unbounded read). It now validates the header and rejects bodies over 64 KiB with 413 before reading.
  • Open pixel: added Cache-Control: no-store (+ Pragma/Expires) so repeated opens are not undercounted by caches.
  • Server shutdown: stop() now joins the background thread with a timeout.
  • Tests: added an SMTPException case for PhishingSender.send (success + connection error were already covered). Full suite: 18 tests pass; isort/black/flake8/ruff clean.

Status

  • CI: all required checks green (GitHub Actions tests, CircleCI ensure_formatting/linter/test/build_docker_images-phishing, codecov). CLA: not required (org member).
  • Threads: 10/10 resolved.
  • Approval: could not be recorded because the reviewing account is the PR author (GitHub blocks self-approval). A second maintainer approval is still required to clear REVIEW_REQUIRED.

Suggested follow-ups (out of scope)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 27 changed files in this pull request and generated 3 comments.

Comment thread phishing/phishing_injector/openaev_phishing.py Outdated
Comment thread phishing/phishing_injector/tracking/server.py
Comment thread phishing/phishing_injector/contracts_phishing.py
Address the second round of review feedback on the native phishing injector:

- Surface per-recipient send failures on partial success. The launch callback
  now appends the failed recipients to the execution message when at least one
  email is sent (status stays SUCCESS so delivered recipients remain
  trackable), instead of dropping the error details whenever sent > 0.
- Guard TrackingServer.stop() behind the started-thread check so calling it
  before start() no longer risks a deadlock from httpd.shutdown() being
  invoked while serve_forever() is not running; server_close() still runs
  unconditionally to release the socket.
- Update the recipients field label to mention semicolons, matching what
  parse_recipients() (and its test) already accept.
- Add unit tests for the partial-failure message and for stop()-before-start().
@SamuelHassine

Copy link
Copy Markdown
Member Author

Second review pass - complete

All three newly landed Copilot threads are addressed in 488f0d4, and an independent full-file re-review found nothing else blocking.

New comments addressed

  • Partial send failures: when at least one email is sent the launch stays SUCCESS (delivered recipients remain trackable), but the failed recipients are now appended to the execution message ("... N/M emails sent ... Failed recipients: : ; ...") instead of being dropped whenever sent > 0.
  • TrackingServer.stop() now guards httpd.shutdown() behind the started-thread check, so calling stop() before start() can no longer deadlock; server_close() still runs to release the socket.
  • Recipients field label now reads "comma, semicolon or newline separated", matching parse_recipients().

Independent re-review

No further blocking findings. Re-confirmed the first-pass hardening is intact: store mutations under the lock, no-store headers on the open pixel, Content-Length cap (413) in do_POST, icon shipped and resolved relative to the module, and execution_output_structured as a single flat json.dumps. Verified the injector's SUCCESS/ERROR status strings match the platform trace-status contract.

Tests / CI

  • 20/20 unit tests pass locally (added coverage for the partial-failure message and for stop()-before-start()).
  • ruff and black clean.
  • All required CI checks are green.

Blocker

Approval could not be recorded here because GitHub does not allow approving your own PR (this gh account is the PR author). A second maintainer must submit the approving review. Everything else is ready.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 27 changed files in this pull request and generated 1 comment.

Comment thread phishing/phishing_injector/tracking/server.py
#319)

The submission endpoint capped Content-Length but never set a socket read
timeout, so a client could advertise a small body and then slow-send or stall
it, pinning a handler thread indefinitely (slowloris-style) and defeating the
size cap. Set a per-request timeout on the tracking handler so a stalled read
raises and frees the thread. Add a unit test asserting the timeout is applied.
@SamuelHassine

Copy link
Copy Markdown
Member Author

Third review pass - complete

Independent full-file re-review (not just the diff). One open thread remained from the previous round - the slowloris gap on the submission endpoint - and it is now fixed.

Addressed

  • Slowloris / missing socket timeout (2fbd749): the submission endpoint capped Content-Length but never set a socket read timeout, so a client could advertise a small body and then slow-send or stall it, pinning a handler thread indefinitely and defeating the size cap. The tracking handler now sets a per-request socket timeout (REQUEST_TIMEOUT_SECONDS = 10), applied by StreamRequestHandler via socket.settimeout(), so a stalled read raises and frees the thread. Added a unit test asserting the timeout is applied to the built handler.

Status

  • Threads: 14/14 resolved.
  • CI: all required checks green (GitHub Actions test-phishing + the rest of the matrix, CircleCI ensure_formatting/linter/test/build_docker_images, codecov). CLA: not required (organization member).
  • Tests: 21/21 unit tests pass locally; isort/black/flake8/ruff clean.

Blocker

Approval could not be recorded here because GitHub does not allow approving your own PR (this gh account is the PR author), which is why the PR still shows REVIEW_REQUIRED / BLOCKED. A second maintainer must submit the approving review; everything else is ready.

Follow-ups (out of scope)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 27 changed files in this pull request and generated 1 comment.

Comment thread phishing/phishing_injector/tracking/server.py
…meout (#319)

The credential-submission endpoint rejected an invalid or oversized
Content-Length with 413 but left the connection open without draining the
body, so leftover bytes on a keep-alive socket could desynchronize the next
request. It also read the body with a per-request socket timeout in effect,
so a slow/stalled client would raise a socket timeout that bubbled up to
BaseHTTPRequestHandler and logged a traceback.

do_POST now forces close_connection on the 413 path and catches read
timeouts (TimeoutError/OSError) to close the connection cleanly instead of
raising. Added a raw-socket test asserting an oversized Content-Length is
rejected with 413, the submit is not recorded, and the server closes the
connection.
@SamuelHassine

Copy link
Copy Markdown
Member Author

Fourth review pass - complete

Independent full-file re-review (not just the diff). The one open thread from the previous round - the 413 rejection path on the submission endpoint - is now fixed.

Addressed

  • Connection desync + read-timeout traceback on the submit endpoint (06d9960): rejecting an invalid/oversized Content-Length returned 413 but left the (undrained) connection open, which could desynchronize a keep-alive client (leftover bytes read as the next request); and the timed rfile.read() could raise a socket timeout that bubbled up to BaseHTTPRequestHandler and logged a traceback. do_POST now sets close_connection = True on the 413 path and catches the timed read (TimeoutError/OSError) to close the connection cleanly. Added a raw-socket unit test asserting an oversized Content-Length yields 413, records no submit, and closes the connection.

Status

  • Threads: 15/15 resolved.
  • CI: all required checks green (GitHub Actions test-phishing + the rest of the matrix, CircleCI ensure_formatting/linter/test/build_docker_images, codecov). CLA: not required (organization member).
  • Tests: 22/22 unit tests pass locally; isort/black/flake8/ruff clean.

Blocker

Approval could not be recorded here because GitHub does not allow approving your own PR (this gh account is the PR author), which is why the PR still shows REVIEW_REQUIRED / BLOCKED. A second maintainer must submit the approving review; everything else is ready.

Follow-ups (out of scope)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build the fully native phishing injector

3 participants