Skip to content

Security policy

← Project README · Contributing · Documentation home · Privacy notice

Security reporting, supported versions, and operating boundaries for European Tech Opportunities 2027.

Reporting a vulnerability

Do not disclose security vulnerabilities in a public issue, discussion, pull request, or listing suggestion.

Channel Contact
GitHub private vulnerability reporting Open a private security advisory
Email simonesiega1@gmail.com

For email reports, use the subject [SECURITY] Brief vulnerability summary.

Please include:

  • a concise summary and potential impact;
  • the affected command, workflow, website surface, or trust boundary;
  • a minimal deterministic reproduction using synthetic or redacted data;
  • required configuration, permissions, and attacker access;
  • OS, runtime, project version or commit, and execution path;
  • a suggested mitigation, if available.

Do not send credentials, cookies, sessions, private HTML, complete environment files, production databases, SQLite sidecars, or unredacted private paths and headers.

What to expect

The maintainer will make a reasonable effort to acknowledge complete reports, reproduce the issue, assess affected versions, coordinate a fix and regression tests, and agree on disclosure timing.

Response time depends on severity and reproducibility; no service-level agreement is provided. Avoid public disclosure until remediation or coordinated disclosure.

Supported versions

Security fixes target main for the current stable release line, starting with v1.0.0. Historical commits are not maintained as separate support lines. See the v1.0.0 release notes for release scope and compatibility.

Security boundaries

Surface Security contract
Source access Authorized public LinkedIn guest HTML only
Authentication No LinkedIn credentials, sessions, cookies, account tokens, or browser storage; source response cookies are discarded
Transport Fixed HTTPS hosts, redirects never followed, redirects other than public-listing 301 treated as stop conditions, and bounded pacing, concurrency, retries, timeouts, and response sizes
Processing Local deterministic parsing and classification with sanitized errors
Persistence Canonical SQLite writes through repository transactions and Alembic migrations
Website and public API Read-only SQLite, validated links, bounded API input and output, no mutation API, and defensive production headers
README Bounded visible projection plus a public-directory review seal, generated with atomic replacement
Public exports Fixed field allowlist, spreadsheet-safe CSV text, atomic replacement, and read-only delivery
Automation Offline validation separated from authorized collection, verified durable snapshots, no canonical state in Actions cache or artifacts, job-scoped permissions, sanitized handoffs, locked deployment, public CI container SBOM/build evidence with main-only attestations (not image signing), and independent scheduled Scorecard reporting to code scanning and the public Scorecard API
Containers Unprivileged processes, explicit mounts, pinned images, and reduced runtime tooling

Detailed behavior is documented in Architecture, Configuration, Database, Automation, and Containers and deployment.

Collection boundary

LinkedIn requests remain blocked unless the relevant authorization interlock is enabled. The interlock records an operator decision; it does not grant permission.

Operators must obtain and retain express authorization and follow applicable policies, terms, laws, and project limits.

The project does not implement or accept:

  • LinkedIn credentials, sessions, cookies, authentication headers, or account tokens;
  • logged-in or browser-based collection;
  • CAPTCHA solving, challenge bypass, proxy rotation, or fingerprint evasion;
  • private or internal APIs;
  • redirect-based endpoint discovery;
  • collection from unrelated providers.

Never weaken or default-enable an authorization gate. An upstream block or challenge is a stop condition.

Public-listing redirects

HTTP 301 returned directly by an approved https://www.linkedin.com/jobs/view/<numeric-id> request preserves that listing as inconclusive without stopping unrelated checks. Its public fields and lifecycle remain unchanged; a completed availability attempt can advance only its audit schedule.

The destination is ignored, even when missing, malformed, or untrusted. The transport never follows it, reads the redirect response body, retries the listing, or uses the redirect as availability or closure evidence. Authorization and original-request endpoint validation remain unchanged.

All redirects from search or guest-detail endpoints, every other redirect status (including 302, 307, and 308), authentication denials (401 and 403), rate limits (429), and access or verification pages still stop source requests.

Rate limits and partial updates

HTTP 429 stops all new source requests, including retries. If it occurs during the availability audit, nightly scraping does not start. Requests already in flight may finish.

Completed work can qualify for partial publication under these conditions:

Operation Required completed work Permitted incomplete work
Collection At least one successful search, including an empty search HTTP 429 failures and searches skipped after the stop
Availability audit At least one confirmed available or unavailable listing HTTP 429 denials, unprocessed listings, and approved numeric public-listing HTTP 301 responses; no other inconclusive or failed check

Approved public-listing HTTP 301 responses preserve their listings as inconclusive and do not block otherwise eligible audit publication. They do not count as confirmed available or unavailable listings.

Other denials, invalid HTML, timeouts, transport or HTTP errors, and unexpected failures block partial publication, even alongside HTTP 429 and public-listing 301 responses. With no qualifying completed work, publication is also blocked. Failed and skipped work supplies no closure evidence; denied and unprocessed rows keep their scheduling priority.

Eligible commands return 4. Integrity, projection, and verified-snapshot checks must still pass, followed by manual review and merge. Collection also requires its quality gate. Partial updates never establish a full-registry quality baseline, and audit-only updates produce no collection-quality report. Other audit source stops return 1; inconclusive audits without a source denial retain exit 2.

Review authorization before any later execution. Partial publication never permits another source phase, retry, or bypass in the stopped execution.

Data and website boundary

data/opportunities.db contains public listing metadata and sensitive operational history. It must never contain credentials, sessions, or authenticated HTML.

Public CSV and JSON exports may contain only LinkedIn job ID, company, title, location, canonical listing URL, category, industries, employment type, and start date. They must exclude status, timestamps, provenance, run history, closure evidence, diagnostics, database paths, and environment values. The separate public dataset metadata may contain only its schema version, UTC generation time, total and employment-type counts, and SHA-256 hashes of the two sanitized downloads. The read-only listing API separately includes the website-visible first-seen timestamp with those listing fields, and excludes all other lifecycle and operational fields.

The read-only /api/v1/status endpoint has a separate five-field allowlist: latest successful collection time, dataset generation time, open-listing count, public JSON download SHA-256, and a validated opaque dataset release identifier (or null in fixed-path mode). It exposes no database checksum, filesystem path, environment value, private run history, or application build configuration. It performs no source requests or canonical writes.

The website must not:

  • run collection, migrations, lifecycle writes, or export generation;
  • expose mutation endpoints;
  • expose database paths, environment values, stack traces, or server configuration;
  • render database values as untrusted raw HTML;
  • emit opportunity links outside validated canonical HTTPS LinkedIn listing URLs matching their numeric job IDs;
  • serve arbitrary filesystem paths or export filenames.

Authentication, forms, user content, server-stored applications, write APIs, or administration interfaces require explicit architecture and security review. Existing browser-local saved/applied/hidden marks remain separate from canonical state; see Privacy.

What to report

Report issues that could:

  • bypass source authorization or request bounds;
  • expose credentials, private state, paths, headers, or environment values;
  • request unexpected hosts or follow unsafe redirects;
  • cause code execution, injection, path traversal, arbitrary file access, or unsafe filesystem writes;
  • let malformed source data corrupt lifecycle state or public output;
  • let failed searches close or mutate listings;
  • corrupt or silently discard canonical state during migration, backup, restore, or deployment;
  • publish private state through logs, caches, artifacts, images, or workflows;
  • compromise dependencies, Actions, containers, snapshots, or deployment credentials;
  • make the website write SQLite, expose internal data, or omit required browser protections.

Usually not a security issue

The following normally belong in a sanitized public issue unless they cross a security boundary:

  • missed or misclassified listings;
  • distinct duplicate IDs;
  • source markup changes;
  • stale generated projections;
  • conservative delayed closure;
  • ordinary 403, 429, timeout, or challenge responses.

Secrets and release security

Never commit or publish:

  • .env contents or tokens;
  • LinkedIn credentials, cookies, sessions, or browser storage;
  • GitHub, package, deployment, or SSH credentials;
  • private proxy or host configuration;
  • authenticated HTML;
  • production databases or sidecars.

Use synthetic placeholders and minimal sanitized fixtures.

If a secret is exposed, revoke or rotate it immediately, remove it from current files and repository history where appropriate, and review logs, artifacts, caches, and deployments. A later deletion commit is not sufficient.

For dependencies and releases:

  • commit uv.lock and site/bun.lock, and use frozen installs;
  • pin Actions and CI tool images to immutable revisions where practical;
  • review weekly Dependabot updates for Python, website, Actions, and Docker inputs;
  • lint workflows and Dockerfiles, run CodeQL security analysis for Python and TypeScript, scan commits with Gitleaks, review new pull-request dependencies for high or critical vulnerabilities, reject fixable high or critical image vulnerabilities, and verify production security headers in CI;
  • keep version references synchronized across metadata, lockfiles, images, user agents, and documentation;
  • publish only from a clean, validated tree;
  • protect deployment keys, artifacts, caches, snapshots, backups, and package credentials with least privilege;
  • keep VPS secrets only in main-restricted canonical-state and production GitHub environments, delete repository-level copies, and remove this repository's access to equivalent organization secrets;
  • keep canonical processing read-only to GitHub, isolate repository mutation in scoped jobs, and never place VPS credentials or canonical SQLite state in pull-request jobs, Actions cache, or artifacts;
  • grant the README mutation job actions: write only so it can dispatch and await validation on its generated commit, and require exact README-only pull-request scope before that dispatch.

Responsible disclosure

Test only systems and data you own or are explicitly authorized to test.

Minimize requests and data access, prefer local fixtures, avoid affecting other users or availability, and stop after demonstrating the issue.

This policy does not authorize testing against LinkedIn, GitHub, the production VPS, or any third party. Never use a vulnerability to bypass access controls. Coordinate disclosure with the maintainer.