App-Envdoctor

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN


## What it detects

Reconciles variables **used** in Perl source (`$ENV{X}`, `$ENV{'X'}`,
`$ENV{"X"}`) against those **defined** in `.env` files:

It also treats interpolated variables in **Docker Compose**
(`docker-compose.yml`, `compose.yaml`, …), **GitHub Actions** workflows
(`.github/workflows/*.yml`), and **Kubernetes** manifests (any `*.yml`/`*.yaml`
with `apiVersion:` and `kind:`) as *used*. `${VAR}` / `$VAR` interpolation is
recognised everywhere (escaped `$$` is ignored), and in Actions the
`secrets.X` / `vars.X` / `env.X` contexts are recognised too. This feeds the
existing missing/undefined and unused detectors — no new rules, and values are
never read from these files. Parsing is dependency-free (regex/line scanning,
no YAML library).

| Rule | Severity | Meaning |
|------|----------|---------|
| `undefined-in-source` | error | Used in code but not defined in any `.env` file |
| `duplicates` | error | Same key defined 2+ times in a single `.env` file |
| `public-prefix` | error | Secret-looking variable exposed to client bundles via a public prefix (`NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, …) |

lib/App/Envdoctor/Scanner.pm  view on Meta::CPAN

        return 'actions';
    }
    if ( $base =~ /\.ya?ml$/i ) {
        return 'k8s'
            if $content =~ /^apiVersion:/m && $content =~ /^kind:/m;
    }
    return undef;
}

# Extract used variable NAMES (with first-by-offset origin) from an infra file.
# Dependency-free: escaped `$$` neutralised, then interpolation (+ actions
# contexts) scanned by regex. Returns { NAME => { file => $rel, line => N } }.
sub scan_infra {
    my ( $rel, $content, $type ) = @_;
    my $text = $content;
    $text =~ s/\$\$/  /g;    # neutralise escaped $$ (preserve offsets)

    my @res;                 # [ start, name ]
    my @patterns = (
        qr/\$\{([A-Za-z_][A-Za-z0-9_]*)/,
        qr/\$([A-Za-z_][A-Za-z0-9_]*)/,
    );
    push @patterns, qr/\b(?:secrets|vars|env)\.([A-Za-z_][A-Za-z0-9_]*)/
        if defined $type && $type eq 'actions';

    for my $re (@patterns) {

t/scanner.t  view on Meta::CPAN

    close $w;

    my $f = App::Envdoctor::Scanner::scan("$dir");
    my %err = map { $_->{name} => $_ } grep { $_->{rule} eq 'undefined-in-source' } @$f;
    ok( $err{COMPOSE_SECRET}, 'COMPOSE_SECRET referenced from compose' );
    ok( $err{DEPLOY_KEY},     'DEPLOY_KEY referenced from actions secrets context' );
    ok( $err{REGION},         'REGION referenced from actions vars context' );
    is( $err{COMPOSE_SECRET}{message},
        'referenced but not defined in any environment file',
        'undefined-in-source uses new message' );
    ok( !$err{NOT_A_VAR}, 'escaped $$ not treated as a variable' );
    ok( !( grep { $_->{rule} eq 'unused' && $_->{name} eq 'DB_URL' } @$f ),
        'DB_URL used via compose interpolation, not unused' );
    my $blob = join "\n", map { $_->{message} } @$f;
    ok( $blob !~ /postgres/, 'no values leak from infra scanning' );
}

# ---- Kubernetes manifest interpolation --------------------------------------
{
    my $dir = File::Temp->newdir;
    open my $k, '>', File::Spec->catfile( "$dir", 'deploy.yaml' ) or die $!;



( run in 1.814 second using v1.01-cache-2.11-cpan-54e63673c56 )