App-Envdoctor

 view release on metacpan or  search on metacpan

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

    return $name =~ /SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|API_?KEY|ACCESS_?KEY|AUTH/i
        ? 1
        : 0;
}

sub _read {
    my ($file) = @_;
    open my $fh, '<', $file or die "cannot read $file: $!";
    local $/;
    my $data = <$fh>;
    close $fh;
    return $data;
}

sub _rel {
    my ( $root, $path ) = @_;
    return File::Spec->abs2rel( $path, $root );
}

sub _find_files {
    my ( $root, $want ) = @_;
    my @out;
    File::Find::find(
        sub {
            return unless -f $_;
            my $full = $File::Find::name;
            return if $full =~ m{/(?:\.git|blib|vendor|node_modules|target)/};
            my $name = $_;
            if ( $want eq 'env' ) {
                push @out, $full
                    if $name eq '.env'
                    || ( $name =~ /^\.env\./ && $name !~ /\.example$/ );
            }
            elsif ( $name =~ /\.p[lm]$/ ) {
                push @out, $full;
            }
        },
        $root
    );
    return sort @out;
}

# Classify an infra file by its relative path + basename + content.
# Returns 'compose', 'actions', 'k8s', or undef.
sub classify_infra {
    my ( $rel, $base, $content ) = @_;
    my $norm = $rel;
    $norm =~ s{\\}{/}g;
    return 'compose' if $base =~ /^(?:docker-)?compose(?:[.-].*)?\.ya?ml$/i;
    if ( $norm =~ m{(?:^|/)\.github/workflows/} && $base =~ /\.ya?ml$/i ) {
        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) {
        while ( $text =~ /$re/g ) {
            my $name  = $1;
            my $start = pos($text) - length($&);
            push @res, [ $start, $name ];
        }
    }

    my %used;
    for my $r ( sort { $a->[0] <=> $b->[0] } @res ) {
        my ( $start, $name ) = @$r;
        next if exists $used{$name};
        my $pre  = substr( $text, 0, $start );
        my $line = ( $pre =~ tr/\n// ) + 1;
        $used{$name} = { file => $rel, line => $line };
    }
    return \%used;
}

# Find infra files (compose / actions / k8s) under root, in sorted order.
# Returns list of [ full, rel, type ].
sub _find_infra {
    my ($root) = @_;
    my @out;
    File::Find::find(
        sub {
            return unless -f $_;
            my $full = $File::Find::name;
            return if $full =~ m{/(?:\.git|blib|vendor|node_modules|target)/};
            my $base = $_;
            return unless $base =~ /\.ya?ml$/i;
            my $rel     = _rel( $root, $full );
            my $content = _read($full);
            my $type    = classify_infra( $rel, $base, $content );
            push @out, [ $full, $rel, $type ] if defined $type;
        },
        $root
    );
    return sort { $a->[1] cmp $b->[1] } @out;
}

# Map each environment label to a hashref set of variable names defined in it.
sub defined_by_label {
    my ($root) = @_;
    my %labels;
    for my $f ( _find_files( $root, 'env' ) ) {
        my ( undef, undef, $base ) = File::Spec->splitpath($f);
        my $label = env_label($base);
        next unless defined $label;
        my $d = parse_env( _rel( $root, $f ), _read($f) );
        $labels{$label}{$_} = 1 for keys %$d;



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