Skip to content
End To End Tester

Security Testing

What automated security testing can and cannot find, the scans worth having in every pipeline, and why business-logic flaws remain a human problem.

2 min read · updated 19 September 2026

Automated security testing finds known-shaped problems. That is a genuinely valuable and genuinely bounded thing, and being clear about the boundary is the most useful contribution this page can make.

It will find a vulnerable dependency, a committed AWS key, a missing security header, a SQL string concatenation and an out-of-date base image.

It will not find that your refund endpoint lets a customer refund somebody else's order. That is a business-logic flaw, it is the category most serious breaches fall into, and no scanner understands your domain well enough to see it.

#The four checks every pipeline should have

All four are fast, cheap and catch recurring real problems.

yaml
# .github/workflows/security.yml
name: Security
on: [push, pull_request]

jobs:
  dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Fail on high and critical only: fail on everything and the team
      # learns to bypass the gate, which is worse than no gate.
      - run: npm audit --audit-level=high

  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }      # scan history, not just the tip
      - uses: gitleaks/gitleaks-action@v2

  static-analysis:
    runs-on: ubuntu-latest
    permissions: { security-events: write }
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with: { languages: javascript-typescript, queries: security-extended }
      - uses: github/codeql-action/analyze@v3

  container:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t app:ci .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:ci
          severity: CRITICAL,HIGH
          exit-code: '1'

Fail on high and critical only. A gate that fails on every low-severity advisory teaches people to bypass the gate, and a bypassed gate is worse than no gate because it looks like protection.

Scan the full history for secrets. A key removed in a later commit is still in the repository and still valid.

#Tests you write yourself

These are the ones that matter most, because they encode what your application means.

Authorization — the same request as two users, asserting one succeeds and one does not. The single highest-value security test most teams are missing, and it gets its own page.

Input handling at the boundary:

typescript
it.each([
  ["'; DROP TABLE orders; --", 'sql'],
  ['<img src=x onerror=alert(1)>', 'xss'],
  ['../../../../etc/passwd', 'traversal'],
  ['{{7*7}}', 'template'],
  ['\u0000truncated', 'null byte']
])('stores %s as literal text (%s)', async (payload) => {
  const { body } = await request(app)
    .post('/orders')
    .send({ sku: 'book-1', quantity: 1, note: payload })
    .expect(201);

  const { body: read } = await request(app).get(`/orders/${body.id}`).expect(200);

  expect(read.note).toBe(payload);        // round-trips exactly: no mangling, no execution
});

Security headers:

typescript
it('sets the headers a browser needs to defend itself', async () => {
  const response = await request(app).get('/').expect(200);

  expect(response.headers['content-security-policy']).toMatch(/default-src 'self'/);
  expect(response.headers['strict-transport-security']).toMatch(/max-age=\d{7,}/);
  expect(response.headers['x-content-type-options']).toBe('nosniff');
  expect(response.headers['referrer-policy']).toBeDefined();
  expect(response.headers['x-powered-by']).toBeUndefined();
});

Rate limiting and account protections:

typescript
it('locks out after five failed sign-ins', async () => {
  for (let i = 0; i < 5; i++) {
    await request(app).post('/signin').send({ email, password: 'wrong' }).expect(401);
  }

  // The correct password now fails too — proof the lockout is real.
  await request(app).post('/signin').send({ email, password: correct }).expect(429);
});

Session and token handling: that a signed-out session is genuinely revoked, that an expired token is rejected even with a valid signature, that changing a password invalidates other sessions.

#DAST

Dynamic scanning attacks a running instance. Worth running on a schedule rather than on every pull request — it is slow and noisy.

yaml
- name: OWASP ZAP baseline scan
  uses: zaproxy/[email protected]
  with:
    target: 'https://staging.example.com'
    rules_file_name: '.zap/rules.tsv'     # tune out the known false positives

Run it against a staging environment, never production, and never against a third party without written authorisation.

#What automation cannot do

The categories that require a person thinking about your domain:

  • Broken access control — top of the OWASP list for years, precisely because it is application-specific. Partly addressable by authorization tests you write.
  • Business-logic flaws — negative quantities producing a credit, a discount code applied twice, a race between two refund requests.
  • Insecure design — the feature is working as specified and the specification is the vulnerability.
  • Chained low-severity issues that combine into something serious.

For these: threat modelling at design time, security review at code review time, and periodic penetration testing by people whose job it is. Automation raises the floor; it does not raise the ceiling.

#The honest summary

Put the four scans in the pipeline this afternoon — they are an hour of work and they will find something. Write authorization tests for every endpoint that returns someone's data. Then accept that the remaining risk is a human-judgement problem, and budget for human judgement.

A green security pipeline means no known-shaped problems were found. It does not mean the system is secure, and the gap between those two statements is where the serious incidents live.

Common questions

Can security testing be fully automated?
No. Automated tools find known-shaped problems — vulnerable dependencies, missing headers, injection in obvious places, committed secrets. They cannot find business-logic flaws, which is the category most damaging breaches fall into, because those require understanding what the application is supposed to mean.
What security checks should every pipeline have?
Dependency scanning, secret scanning, static analysis with security rules, and container image scanning. All four are cheap, run in under a minute, and catch a real and recurring class of problem.
What is the difference between SAST and DAST?
SAST reads your source code looking for dangerous patterns; DAST attacks a running instance from the outside. SAST runs early and produces false positives; DAST runs late and finds only what it can reach. They find different things and both are worth having.

Runnable samples for this page

last test results ↗

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?