angular

Seeded Angular defects — a disabled sanitiser, dead subscriptions, an open redirect, work in a template binding.

What this is

15 deliberate defects, each carrying a marker naming the rule in standards/ it violates. It measures recall, and its pass condition is that every one of the 9 BLOCKER markers is flagged. The rule id in each marker means the scorer grades attribution too: a reviewer that finds the bug but cites the wrong rule counts as caught, and separately as misattributed.

How to onboard it

Nothing is installed and nothing here is ever merged. The corpus is used by opening a throwaway pull request on a pilot repository already onboarded for this stack, adding this directory and seeded/clean together. Label it redline-exempt so the readiness gate does not block a pull request nobody will merge, wait for the automated review to finish, then score it.

seed-canary.yml does exactly this on a schedule and closes the pull request afterwards, including when the run fails.

How to use it

terminal
$ GH_TOKEN=... npx redlinegate metrics score-seeds --repo <org>/<repo> --pr <n>
SeedSeverityRule it violatesDefect
1BLOCKERcore/hardcoded-secretsapi key committed in source
2BLOCKERcore/escape-hatch-typesany-typed input
3BLOCKERangular/bypass-security-trustsanitiser switched off on user-supplied html
4BLOCKERangular/open-redirect-navigationnavigation target taken straight from a query param
5BLOCKERangular/unsubscribed-subscriptionsubscription with no teardown
6BLOCKERangular/uncancelled-request-racea request per keystroke, merged not switched
8BLOCKERangular/timer-not-clearedinterval never cleared in ngOnDestroy
9BLOCKERcore/customer-data-in-logsmsisdn written to the console
16BLOCKERangular/open-redirect-navigationrouterLink bound to a query-param url -->
7HIGHangular/interceptor-swallows-errorfailure returned as an empty success
10HIGHangular/unvalidated-route-paramroute param coerced with no validation
12HIGHangular/input-object-mutationchild writing to an object the parent owns
14HIGHangular/function-call-in-templatemethod called from a binding, so it runs every cycle -->
15HIGHangular/missing-trackbyngFor over a re-fetched list with no trackBy -->
13SUGGESTIONangular/prefer-async-pipemanual subscribe where the async pipe would do

Expected output

A score from scripts/score-seeds.mjs: BLOCKER recall, false positives, and rule attribution. The canary appends it to data/seed-scores.jsonl and fails the run if BLOCKER recall drops below 1.0 or the clean corpus attracts a false positive. Of the 15 defects here, 9 BLOCKER, 5 HIGH and 1 SUGGESTION — only the BLOCKER count is a pass condition.

How to edit it

Add a defect by adding the code and one marker line above it. There is no separate expectations file to keep in sync — scripts/score-seeds.mjs parses the markers straight out of the source:

marker format
SEED <n> [BLOCKER|HIGH|SUGGESTION] (<stack>/<rule-slug>) <short description>

scripts/validate.mjs fails CI if a seed cites a rule that does not exist, or claims a severity higher than that rule carries in the standard — so a marker cannot quietly drift away from the rule it is testing.

The full file

seeded/angular/seeded-violations.component.ts · 91 lines · 3.7 KB
// DO NOT MERGE — Redline validation seed.
// Every `SEED n [SEVERITY] (rule-id)` marker must be flagged at that severity or higher,
// citing that rule id. Score with scripts/score-seeds.mjs.
import { Component, Input, OnInit } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, mergeMap, of } from 'rxjs';

// SEED 1 [BLOCKER] (core/hardcoded-secrets) api key committed in source
const PARTNER_API_KEY = 'ptnr-live-51H8xQ2Kd9Wm4Tz6Hb';

@Component({
  selector: 'app-seeded-violations',
  template: `
    <div [innerHTML]="trustedBio"></div>
    <!-- SEED 14 [HIGH] (angular/function-call-in-template) method called from a binding, so it runs every cycle -->
    <p>{{ formatTotal() }}</p>
    <!-- SEED 15 [HIGH] (angular/missing-trackby) ngFor over a re-fetched list with no trackBy -->
    <li *ngFor="let order of orders">{{ order.reference }}</li>
    <!-- SEED 16 [BLOCKER] (angular/open-redirect-navigation) routerLink bound to a query-param url -->
    <a [routerLink]="returnUrl">back</a>
  `,
})
export class SeededViolationsComponent implements OnInit {
  // SEED 2 [BLOCKER] (core/escape-hatch-types) any-typed input
  @Input() profile: any;

  orders: { reference: string }[] = [];
  trustedBio: unknown;
  returnUrl = '/';

  constructor(
    private readonly sanitizer: DomSanitizer,
    private readonly http: HttpClient,
    private readonly route: ActivatedRoute,
    private readonly router: Router
  ) {}

  ngOnInit(): void {
    // SEED 3 [BLOCKER] (angular/bypass-security-trust) sanitiser switched off on user-supplied html
    this.trustedBio = this.sanitizer.bypassSecurityTrustHtml(this.profile.bio);

    // SEED 4 [BLOCKER] (angular/open-redirect-navigation) navigation target taken straight from a query param
    this.returnUrl = this.route.snapshot.queryParams['next'];
    this.router.navigateByUrl(this.returnUrl);

    // SEED 5 [BLOCKER] (angular/unsubscribed-subscription) subscription with no teardown
    this.http.get<{ reference: string }[]>('/api/orders').subscribe((orders) => {
      this.orders = orders;
    });

    // SEED 6 [BLOCKER] (angular/uncancelled-request-race) a request per keystroke, merged not switched
    this.route.queryParams
      .pipe(
        mergeMap((params) => this.http.get<{ reference: string }[]>(`/api/search?q=${params['q']}`)),
        // SEED 7 [HIGH] (angular/interceptor-swallows-error) failure returned as an empty success
        catchError(() => of([]))
      )
      .subscribe((orders) => (this.orders = orders));

    // SEED 8 [BLOCKER] (angular/timer-not-cleared) interval never cleared in ngOnDestroy
    setInterval(() => this.http.get('/api/heartbeat').subscribe(), 5000);

    // SEED 9 [BLOCKER] (core/customer-data-in-logs) msisdn written to the console
    console.log('loaded profile for', this.profile.msisdn);

    // SEED 10 [HIGH] (angular/unvalidated-route-param) route param coerced with no validation
    const accountId = +this.route.snapshot.params['accountId'];
    this.http.get(`/api/accounts/${accountId}`).subscribe();
  }

  formatTotal(): string {
    return this.orders.reduce((sum, o) => sum + o.reference.length, 0).toFixed(2);
  }

  // SEED 12 [HIGH] (angular/input-object-mutation) child writing to an object the parent owns
  markSeen(): void {
    this.profile.seen = true;
  }

  // SEED 13 [SUGGESTION] (angular/prefer-async-pipe) manual subscribe where the async pipe would do
  refresh(): void {
    this.http.get<{ reference: string }[]>('/api/orders').subscribe((o) => (this.orders = o));
  }

  authHeader(): string {
    return `Bearer ${PARTNER_API_KEY}`;
  }
}