GitHub And Open Source Collaboration

GitHub Security Features And Alert Response

Use GitHub's official documentation for current platform behavior. GitHub documents Dependabot alerts, secret scanning, code scanning, and security advisories. Which features are free, which require GitHub Advanced Security, and exact configuration screens change; the stable lesson is what each signal means and how to build a response process around it.

Three Different Detectors

The three features answer three different questions, and conflating them is the most common mistake:

  • Dependabot alerts ask "does a dependency I've declared have a known vulnerability?" It compares your composer.lock (and other manifests) against public vulnerability databases and opens an alert per affected dependency, with a severity and, often, a suggested fixed version. Dependabot can also open pull requests that bump the version automatically.
  • Secret scanning asks "did a credential get committed to this repository?" It pattern-matches commits (and, on public repos and with push protection enabled, pushes themselves) against known secret formats — API keys, tokens, private keys — and can also partner with providers to auto-revoke some leaked credentials.
  • Code scanning (commonly powered by CodeQL) asks "does the code itself contain a known vulnerability pattern?" — SQL built from unescaped input, a deserialization sink, a path traversal shape — by analyzing the code, not the dependency graph or the commit history.

A project can have a clean Dependabot tab, a clean secret-scanning tab, and still ship a SQL injection that only code scanning would catch, or the reverse. Treat the three as complementary, not redundant.

Dependabot In A PHP Project

For Composer-based PHP projects, Dependabot reads composer.json/composer.lock and cross-references the advisory database. A typical alert names the vulnerable package, the vulnerable version range, the patched version, and a severity. The response is one of a small set of moves:

  • Upgrade to the patched version — the default, correct response when the fix is a drop-in replacement.
  • Pin and mitigate when the patched version has breaking changes you can't absorb immediately — apply a compensating control (disable the vulnerable code path, add input validation upstream) and track the upgrade as follow-up work, not as closed.
  • Dismiss with a reason when the vulnerable code path is genuinely unreachable in your application (a vulnerability in a CLI tool your project depends on but never invokes) — GitHub records dismissal reasons, and a dismissal without one is indistinguishable from an alert nobody read.

Dependabot can also be configured (via a dependabot.yml file) to open version-bump pull requests automatically on a schedule, separate from security alerts — useful for staying current, but a bot-opened PR still needs the same review and CI gate as a human-authored one; auto-merging dependency bumps without tests is how a "routine" update becomes an outage.

Secret Scanning And Push Protection

A secret committed to Git history is compromised the moment it's pushed, regardless of whether it's later removed — anyone who cloned or fetched in between has it, and Git history retains it unless the history itself is rewritten. Secret scanning exists because "don't commit secrets" is a rule people break under deadline pressure, not a rule that needs re-teaching.

Two distinct capabilities matter:

  • Detection finds secrets already in the repository's history and raises an alert.
  • Push protection (where enabled) blocks the push before the secret lands in history at all — the better outcome, since it prevents the leak instead of reacting to it.

When a real secret is found, the response is not "delete the file and commit again" — the old commit still exists in history and is still fetchable. The response is: rotate the credential first, treating it as compromised regardless of whether anyone is known to have used it; only after rotation does cleaning history (if the repository is public or history exposure itself is a concern) become worth the disruption of a force-push. Rotation is the step that actually removes the risk; history cleanup is hygiene that only matters afterward.

Code Scanning And CodeQL

Code scanning runs static analysis against your source and flags patterns known to be exploitable — the code-level equivalent of what the security-review skill's static-analysis pass looks for, but running automatically on every push and pull request. For PHP specifically, look for CodeQL's PHP query support and any framework-aware static analyzers you already run (PHPStan security-focused rulesets, Psalm's taint analysis) and wire whichever apply into the same security tab or CI gate so findings live in one place.

A typical setup runs code scanning as a required check on pull requests:

name: CodeQL

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: php
      - uses: github/codeql-action/analyze@v3

Findings appear as annotations on the pull request and in the repository's security tab, with severity and a description of the pattern matched. Like Dependabot, each finding needs a real disposition — fixed, mitigated, or dismissed with a reason — not just visibility.

Triage: Turning Alerts Into A Workflow

An alert nobody owns is not a control, it's a dashboard. A working triage process names:

  • Who is notified. GitHub can route alerts by CODEOWNERS or team configuration; confirm the right people actually receive them, not just whoever set up the feature originally.
  • How fast, by severity. A critical remote-code-execution alert on a production dependency deserves a same-day response; a low-severity alert on a dev-only tool can wait for the next sprint. Write the SLA down so "we'll get to it" isn't the default for everything.
  • What "resolved" means. Merged fix, verified mitigation, or a documented, time-bounded acceptance of risk — not just a closed tab.
  • Where the decision is recorded. A dismissal reason, a linked issue, or a comment on the alert itself — so six months later, someone can tell whether an old alert was actually assessed or just ignored.

Failure Modes

The most common failure is enabled but unowned: someone turns on Dependabot, secret scanning, and code scanning during initial repository setup, and no process exists for what happens when they fire. The features report faithfully; nobody reads the report.

The second is alert fatigue from noise: a repository with hundreds of low-severity alerts on transitive dependencies trains reviewers to stop looking, which means the one critical alert buried in the list gets the same treatment as the routine ones. Triage severity aggressively and suppress what's genuinely not actionable, rather than letting volume erode attention.

The third is treating a green security tab as proof of safety. These are automated pattern-matchers against known signatures and known code shapes — they don't find novel vulnerabilities, logic errors, or business-logic authorization bugs. A clean Dependabot and CodeQL run says "no known problems of the kinds these tools check," not "this application is secure."

The fourth is secret leaks handled as a file deletion instead of a rotation. Removing a committed secret from the current file state without rotating the credential leaves the actual exposure open — the credential in the old commit still works.

Review Checklist

  • Dependabot, secret scanning, and code scanning are all enabled, and someone can name who is notified when each fires.
  • A written (even if brief) triage SLA exists per severity level, and recent alerts show it being followed rather than accumulating unaddressed.
  • Push protection is enabled where available, so secrets are blocked before they enter history, not just detected after.
  • At least one past secret-scanning alert can be traced to an actual credential rotation, not just a commit removing the file.
  • Code-scanning findings on recent pull requests were given a real disposition (fixed/mitigated/dismissed with reason), not silently merged past.
  • The team can articulate what these three tools do not catch, so a clean security tab isn't mistaken for a completed security review.

What You Should Be Able To Do

After this lesson, you should be able to explain what Dependabot alerts, secret scanning, and code scanning each detect and don't detect, configure them for a PHP/Composer project, design a triage process that turns alerts into owned decisions with an SLA, and correctly respond to a leaked secret by rotating the credential first rather than treating history cleanup as the fix.

Practice

Triage a Dependabot alert

Separately, an older low-severity alert exists for a dev-only static analysis tool that never runs outside CI and never touches untrusted input.

Write a triage decision for both alerts covering:

  • what action you take for each (upgrade, pin-and-mitigate, or dismiss-with-reason) and why;
  • how fast each should be handled, and why the two deserve different urgency;
  • what evidence you'd leave behind so a reviewer six months from now can tell the alert was actually assessed;
  • what you would do differently if the patched version of guzzlehttp/psr7 did have breaking changes.
Show solution

Solution

The guzzlehttp/psr7 alert: upgrade, handled this week.

  • Action: upgrade to the patched version. It's a drop-in replacement with no breaking changes, so there's no reason to pin-and-mitigate instead — mitigation exists for when the real fix isn't immediately available, and here it is.
  • Urgency: high, relative to the other alert. The package is used across most of the application's HTTP client code, meaning the vulnerable path is broadly reachable, and it's a moderate-severity denial-of-service issue in a library that processes external input (HTTP responses) — exactly the kind of dependency where "we'll get to it" risks an actual incident.
  • Evidence left behind: the pull request bumping the version, linked to the Dependabot alert; CI green on that PR; the alert auto-closes on merge since GitHub can detect the fixed version landed. A one-line note in the PR description ("fixes GHSA-xxxx, no breaking changes per changelog") gives a future reviewer the "why" without them having to re-derive it.

The dev-only static analysis tool alert: dismiss with reason, no urgency.

  • Action: dismiss, with the reason "vulnerable code path not reachable — tool runs only in CI against our own trusted source, never processes untrusted input, and doesn't run in production."
  • Urgency: low — it can be assessed at any point in the next sprint or two, not same-day. Low severity, dev-only, no untrusted input in the path.
  • Evidence left behind: the dismissal reason itself, recorded in GitHub's UI at dismissal time. That's the artifact — a reviewer later can see why it was dismissed, not just that it was, which is what separates a real triage decision from an alert that quietly disappeared.

Why the two get different urgency: severity alone doesn't set priority — reachability and blast radius do. A moderate-severity issue in a broadly-used, internet-facing dependency outranks a low-severity issue in a tool that never sees untrusted data, even though "moderate > low" might suggest otherwise on severity alone. Triage has to weigh both.

If the patched version had breaking changes: don't leave the vulnerability open while you plan a larger migration. Apply a compensating control immediately — for a header-parsing DoS, that might mean adding request size/complexity limits upstream of the library call — and track the real upgrade as a separate, scoped piece of follow-up work with an owner and rough timeline, not as "closed." The alert stays open (or gets a dismissal reason that explicitly says "mitigated, upgrade tracked in #1234, not resolved") rather than being silently dropped once the immediate pressure is off.

Diagnose a leaked-secret response

A developer accidentally commits .env containing a live Stripe secret key to a feature branch, pushes it, and opens a pull request. Secret scanning fires an alert an hour later. By then, the developer has already noticed the mistake themselves, deleted the line from .env, and pushed a follow-up commit removing it — and considers the issue closed since the file "doesn't have the secret anymore."

Diagnose what's actually still wrong with this response, and write the correct sequence of steps, covering:

  • why deleting the line in a follow-up commit does not remove the exposure;
  • the first action that should have been taken, and why it comes before anything else;
  • whether history needs to be rewritten (force-push to remove the commit), and what that decision depends on;
  • what should NOT be done before the credential is confirmed rotated.
Show solution

Diagnosis

What's still wrong: the developer's fix addresses the working tree, not the exposure. The original commit — the one with the live key still in it — remains in the branch's Git history and is fully fetchable by anyone who has the repository URL and read access, regardless of what the latest commit on the branch looks like. git log -p or git show <that-commit> on the old commit still reveals the key. If the repository is public, or if anyone who shouldn't have the key already cloned or fetched during that hour before the follow-up commit, the key has been exposed — and "the file doesn't have it anymore" does nothing to change that.

First action: rotate the credential, immediately, before anything else. The key must be treated as compromised the moment it was pushed, independent of whether anyone is known to have used it maliciously — you cannot prove a negative about who fetched a public or organization-visible ref in the intervening hour. Revoke the leaked Stripe key and issue a new one, update it wherever the application consumes it (environment config, secret store, deployment pipeline), and confirm the application is running on the new key before considering this handled. This step comes first because it's the only one that actually closes the exposure — everything else (history cleanup, process fixes) matters for hygiene and prevention, but the leaked key keeps working until it's revoked.

History rewrite: depends on exposure surface, and it's secondary. If the repository is private with a small, trusted team and you've confirmed rotation happened fast enough that risk is low, rewriting history to scrub the commit is optional cleanup. If the repository is public, or the team can't be confident about who had access during the exposure window, rewriting history (interactive rebase or a tool like git filter-repo, followed by a force-push and coordination with anyone else who has the branch checked out) is worth doing — but only after rotation, since a scrubbed-but-not-rotated key is still a live credential that simply isn't visible anymore, which is arguably worse because it looks resolved.

What must not happen before rotation is confirmed: closing the secret-scanning alert, merging the pull request, or treating the incident as resolved in any tracking system. None of those actions reduce the actual risk — the live key is still valid until Stripe's dashboard shows it revoked and the new key is deployed. Recording "fixed" anywhere before that point creates the exact false confidence this lesson warns about: a clean-looking alert tab while a working, previously-exposed credential remains active.

Review a code-scanning setup
name: CodeQL

on:
  workflow_dispatch:

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: php
      - uses: github/codeql-action/analyze@v3

Write a review of this setup covering:

  • what's missing from the trigger, and why workflow_dispatch-only undermines the point of code scanning;
  • whether this workflow, as configured, can block a pull request from merging, and what would need to change if it should;
  • what a reviewer should check about how findings get triaged, beyond "the workflow runs successfully";
  • one thing this workflow gets right.
Show solution

Review

The trigger is the main problem. on: workflow_dispatch means this workflow only runs when someone manually triggers it from the Actions tab — it does not run on push, and critically, it does not run on pull requests. Code scanning's value comes from catching a vulnerability pattern before it merges, ideally as a check the author sees on their own PR. A manual-only trigger means this workflow will run exactly as often as someone remembers to click the button — which in practice means rarely, and probably not on the PR that actually introduces a problem. The fix is adding push (to main, so the default branch always has current results) and pull_request triggers:

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

As configured, this cannot block a merge. Even with the trigger fixed, code scanning only reports findings unless the check is made a required status check on the branch's protection rules. A workflow that runs and reports findings, with nobody having set it as required, can be entirely merged around: a PR can merge with open critical findings because nothing is stopping it. Fixing the trigger is necessary but not sufficient — the repository's branch protection settings need this workflow's check added as required for the branch the team actually wants protected.

Beyond "the workflow runs successfully," a reviewer should check:

  • Findings are actually looked at, not just produced — pull the security tab's history and confirm recent findings show a real disposition (fixed, dismissed with a reason), not an accumulating unaddressed backlog.
  • Someone is notified when a new finding appears, the same ownership question that applies to Dependabot and secret-scanning alerts — a workflow producing output nobody reads is equivalent to no workflow.
  • The severity threshold and query suite in use are appropriate — the default query pack is a reasonable start, but a team relying entirely on defaults without ever reviewing what's covered may have unwarranted confidence in coverage they don't actually have.

What this workflow gets right: permissions is scoped down to contents: read and security-events: write — the minimum needed for CodeQL to check out code and upload results — rather than leaving default (broader) permissions in place. That's the right instinct even though the trigger and branch-protection gaps mean the workflow isn't yet doing useful work.