Skip to content

Instantly share code, notes, and snippets.

@melezhik
Created May 1, 2026 10:35
Show Gist options
  • Select an option

  • Save melezhik/ebf795b575aabbd0e2db8e7004b3f726 to your computer and use it in GitHub Desktop.

Select an option

Save melezhik/ebf795b575aabbd0e2db8e7004b3f726 to your computer and use it in GitHub Desktop.
Sparrow6 Task Check Logrotate configuration parser
# ============================================================
# Sparrow6 task.check for parsing logrotate configuration files
# ============================================================
# Validates: paths, rotation options, script blocks, permissions
# Supports: /etc/logrotate.conf and /etc/logrotate.d/* syntax
# Uses Raku-style regular expressions with capture groups
# ============================================================
# ============================================================
# SECTION 1: GLOBAL CONFIGURATION DIRECTIVES
# ============================================================
# Match global options at file root level (not inside log blocks)
# Examples: weekly, compress, dateext, use_syslog
~regexp: ^^ \s* (daily|weekly|monthly|yearly) \s* $$
~regexp: ^^ \s* (compress|nocompress|delaycompress|dateext|dateyesterday) \s* $$
~regexp: ^^ \s* create \s+ (\d{3,4}) \s+ (\S+) \s+ (\S+) \s* $$
generator: <<CODE
!perl
my ($mode, $user, $group) = @{capture()};
# Validate octal permission format
my $valid_mode = ($mode =~ /^[0-7]{3,4}$/) ? 1 : 0;
print "assert: $valid_mode create mode '$mode' is valid octal\n";
print "note: create directive: mode=$mode user=$user group=$group\n";
CODE
end:
~regexp: ^^ \s* su \s+ (\S+) \s+ (\S+) \s* $$
generator: <<CODE
!perl
my ($user, $group) = @{capture()};
print "note: su directive: running as $user:$group\n";
# Warn if using root (security consideration)
if ($user eq 'root') {
print "note: WARNING: su root may have elevated privileges\n";
}
CODE
end:
~regexp: ^^ \s* include \s+ ([\w/.-]+) \s* $$
generator: <<CODE
!perl
my $include_path = capture()->[0];
print "note: include directive references: $include_path\n";
# Soft assertion - file existence checked externally
print "assert: 1 include path syntax valid\n";
CODE
end:
# ============================================================
# SECTION 2: LOG FILE PATH BLOCKS
# ============================================================
# Matches: /var/log/*.log { ... } or multiple paths per block
# Start of a log block: one or more file paths followed by {
within: ^^ \s* (/[^\s{]+(?:\s+/[^\s{]+)*) \s* \{ \s* $$
# Capture all paths in the block header
generator: <<CODE
!perl
my $paths_line = capture()->[0];
my @paths = split(/\s+/, $paths_line);
my $count = scalar @paths;
print "note: Log block defines $count path(s):\n";
for my $p (@paths) {
print "note: - $p\n";
# Basic path validation
my $valid = ($p =~ m{^/[\w./%*?-]+$}) ? 1 : 0;
print "assert: $valid path '$p' has valid format\n";
}
# Store paths in state for cross-reference with options
update_state({ log_paths => \@paths, path_count => $count });
CODE
# --------------------------------------------------------
# SUB-CHECKS: Options inside the log block
# --------------------------------------------------------
# Rotation frequency (only one should be set)
~regexp: ^^ \s* (daily|weekly|monthly|yearly) \s* $$
# Rotation count: rotate N
regexp: ^^ \s* rotate \s+ (\d+) \s* $$
generator: <<CODE
!perl
my $count = capture()->[0];
my $valid = ($count >= 0 && $count <= 365) ? 1 : 0;
print "assert: $valid rotate count '$count' is reasonable (0-365)\n";
update_state({ rotate_count => $count });
CODE
end:
# Size-based rotation
~regexp: ^^ \s* (size|maxsize|minsize) \s+ (\d+[kMGT]?) \s* $$
generator: <<CODE
!perl
my ($type, $val) = @{capture()};
print "note: Size directive: $type=$val\n";
CODE
end:
# Age-based rotation
~regexp: ^^ \s* maxage \s+ (\d+) \s* $$
# File handling options (soft checks - all optional)
~regexp: ^^ \s* (missingok|notifempty|ifempty|copytruncate) \s* $$
~regexp: ^^ \s* (compress|nocompress|delaycompress|compresscmd|uncompresscmd) \s* $$
~regexp: ^^ \s* (dateext|dateyesterday|dateformat) \s* $$
~regexp: ^^ \s* (sharedscripts|nosharedscripts) \s* $$
~regexp: ^^ \s* (olddir|extension) \s+ (\S+) \s* $$
# Create directive inside block (overrides global)
regexp: ^^ \s* create \s+ (\d{3,4}) \s+ (\S+) \s+ (\S+) \s* $$
generator: <<CODE
!perl
my ($mode, $user, $group) = @{capture()};
print "note: Block-level create: mode=$mode owner=$user:$group\n";
# Cross-check: warn if mode is world-writable
if ($mode =~ /[0-7]?[0-7][2367]$/) {
print "note: WARNING: create mode $mode may be overly permissive\n";
}
CODE
end:
# --------------------------------------------------------
# SUB-CHECKS: Script blocks (prerotate/postrotate)
# --------------------------------------------------------
# Detect start of script block
within: ^^ \s* (prerotate|postrotate|firstaction|lastaction) \s* $$
generator: <<CODE
!perl
my $script_type = capture()->[0];
print "note: Found $script_type script block\n";
update_state({ has_$script_type => 1 });
CODE
# Match endscript marker (required to close block)
regexp: ^^ \s* endscript \s* $$
generator: <<CODE
!perl
print "note: Script block properly closed with endscript\n";
print "assert: 1 $script_type block has matching endscript\n";
CODE
end:
# Soft check: warn if script contains potentially dangerous patterns
~regexp: (rm\s+-rf\s+/|chmod\s+777|eval\s+\$|`[^`]+`)
generator: <<CODE
!perl
if (matched()) {
print "note: WARNING: Script contains potentially unsafe pattern: " . matched()->[0] . "\n";
}
CODE
end:
end: # Close script block context
# --------------------------------------------------------
# SUB-CHECKS: Validation rules for block completeness
# --------------------------------------------------------
# Assert: every block should have rotate OR size/maxsize
code: <<CODE
!perl
my $state = get_state();
my $has_rotate = defined $state->{rotate_count};
my $has_size = 0; # Would need additional tracking in real usage
# For demo: assume rotate is preferred
unless ($has_rotate) {
print "note: WARNING: Log block missing 'rotate N' directive\n";
}
CODE
end: # Close log block context
# ============================================================
# SECTION 3: SYNTAX & STRUCTURE VALIDATION
# ============================================================
# Check for unmatched braces (basic structural validation)
# Count opening and closing braces across entire file
code: <<CODE
!perl
my $input = get_input(); # Full file content
my $open = () = $input =~ /\{/g;
my $close = () = $input =~ /\}/g;
my $balanced = ($open == $close) ? 1 : 0;
print "note: Structure check: { count=$open, } count=$close\n";
print "assert: $balanced brace pairs are balanced\n";
if (!$balanced) {
print "note: ERROR: Unmatched braces may cause logrotate to fail\n";
}
CODE
# Check for common syntax errors (soft checks)
~regexp: ^^ \s* [^#\s] [^{]* \{ [^}]* $ # Unclosed block on single line
generator: <<CODE
!perl
if (matched()) {
print "note: WARNING: Possible malformed block on line: " . matched()->[0] . "\n";
}
CODE
end:
# Detect comments and skip validation on them (soft check)
~regexp: ^^ \s* # .* $$
# ============================================================
# SECTION 4: SECURITY & BEST PRACTICE CHECKS
# ============================================================
# Warn if log files are in world-writable directories
regexp: ^^ \s* (/tmp|/var/tmp|/dev/shm)/ [^{]+ \s* \{
generator: <<CODE
!perl
my $path = capture()->[0];
print "note: WARNING: Log path '$path' is in world-writable directory\n";
print "note: Consider moving logs to /var/log for better security\n";
CODE
end:
# Warn if compress is disabled for high-volume logs
~regexp: ^^ \s* nocompress \s* $$
generator: <<CODE
!perl
print "note: INFO: Compression disabled - may increase disk usage\n";
CODE
end:
# Check for missing 'missingok' (can cause errors if log disappears)
code: <<CODE
!perl
my $input = get_input();
# Count blocks without missingok
my @blocks = $input =~ m{(/[\s\S]+?\{[\s\S]*?)(?=\n\S|\z)}g;
my $missing_missingok = 0;
for my $block (@blocks) {
next if $block =~ /missingok/;
next if $block =~ /^\s*#/; # Skip comments
$missing_missingok++;
}
if ($missing_missingok > 0) {
print "note: WARNING: $missing_missingok log block(s) missing 'missingok' directive\n";
print "note: Tip: Add 'missingok' to prevent errors when logs are absent\n";
}
CODE
# ============================================================
# SECTION 5: REPORTING & SUMMARY
# ============================================================
# Final summary report after all checks
code: <<CODE
!perl
use JSON;
my $state = get_state();
my $summary = {
paths_checked => $state->{path_count} || 0,
rotate_count => $state->{rotate_count},
has_scripts => [grep { $_ =~ /^has_/ } keys %$state],
status => 'validated'
};
print "\n=== LOGROTATE CONFIG VALIDATION SUMMARY ===\n";
print "Paths defined: $summary->{paths_checked}\n";
print "Rotation count: " . ($summary->{rotate_count} // 'not set') . "\n";
print "Script blocks: " . join(", ", @{$summary->{has_scripts}}) . "\n";
print "Status: $summary->{status}\n";
print "=============================================\n";
# Optional: output structured result for CI/CD pipelines
if (config()->{json_output}) {
print "RESULT_JSON: " . encode_json($summary) . "\n";
}
CODE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment