-
-
Save melezhik/ebf795b575aabbd0e2db8e7004b3f726 to your computer and use it in GitHub Desktop.
Sparrow6 Task Check Logrotate configuration parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================ | |
| # 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