App-Envdoctor
view release on metacpan or search on metacpan
## 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 )