diff --git a/README.md b/README.md index 05e33b991..82f70d890 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,10 @@ ![Python](https://img.shields.io/badge/-Python-3776AB?logo=python&logoColor=white) ![Go](https://img.shields.io/badge/-Go-00ADD8?logo=go&logoColor=white) ![Java](https://img.shields.io/badge/-Java-ED8B00?logo=openjdk&logoColor=white) +![Perl](https://img.shields.io/badge/-Perl-39457E?logo=perl&logoColor=white) ![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown&logoColor=white) -> **50K+ stars** | **6K+ forks** | **30 contributors** | **6 languages supported** | **Anthropic Hackathon Winner** +> **50K+ stars** | **6K+ forks** | **30 contributors** | **5 languages supported** | **Anthropic Hackathon Winner** --- @@ -155,9 +156,9 @@ git clone https://github.com/affaan-m/everything-claude-code.git cd everything-claude-code # Recommended: use the installer (handles common + language rules safely) -./install.sh typescript # or python or golang +./install.sh typescript # or python or golang or perl # You can pass multiple languages: -# ./install.sh typescript python golang +# ./install.sh typescript python golang perl # or target cursor: # ./install.sh --target cursor typescript # or target antigravity: @@ -310,6 +311,9 @@ everything-claude-code/ | |-- liquid-glass-design/ # iOS 26 Liquid Glass design system (NEW) | |-- foundation-models-on-device/ # Apple on-device LLM with FoundationModels (NEW) | |-- swift-concurrency-6-2/ # Swift 6.2 Approachable Concurrency (NEW) +| |-- perl-patterns/ # Modern Perl 5.36+ idioms and best practices (NEW) +| |-- perl-security/ # Perl security patterns, taint mode, safe I/O (NEW) +| |-- perl-testing/ # Perl TDD with Test2::V0, prove, Devel::Cover (NEW) | |-- autonomous-loops/ # Autonomous loop patterns: sequential pipelines, PR loops, DAG orchestration (NEW) | |-- plankton-code-quality/ # Write-time code quality enforcement with Plankton hooks (NEW) | @@ -361,6 +365,8 @@ everything-claude-code/ | |-- typescript/ # TypeScript/JavaScript specific | |-- python/ # Python specific | |-- golang/ # Go specific +| |-- perl/ # Perl specific (NEW) +| |-- swift/ # Swift specific | |-- hooks/ # Trigger-based automations | |-- README.md # Hook documentation, recipes, and customization guide @@ -563,6 +569,7 @@ This gives you instant access to all commands, agents, skills, and hooks. > cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # pick your stack > cp -r everything-claude-code/rules/python/* ~/.claude/rules/ > cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ +> cp -r everything-claude-code/rules/perl/* ~/.claude/rules/ > > # Option B: Project-level rules (applies to current project only) > mkdir -p .claude/rules @@ -588,6 +595,7 @@ cp -r everything-claude-code/rules/common/* ~/.claude/rules/ cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # pick your stack cp -r everything-claude-code/rules/python/* ~/.claude/rules/ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ +cp -r everything-claude-code/rules/perl/* ~/.claude/rules/ # Copy commands cp everything-claude-code/commands/*.md ~/.claude/commands/ @@ -670,6 +678,7 @@ rules/ typescript/ # TS/JS specific patterns and tools python/ # Python specific patterns and tools golang/ # Go specific patterns and tools + perl/ # Perl specific patterns and tools ``` See [`rules/README.md`](rules/README.md) for installation and structure details. @@ -824,7 +833,7 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ### Ideas for Contributions -- Language-specific skills (Rust, C#, Swift, Kotlin) — Go, Python, Java already included +- Language-specific skills (Rust, C#, Kotlin, Java) — Go, Python, Perl, Swift, and TypeScript already included - Framework-specific configs (Rails, Laravel, FastAPI, NestJS) — Django, Spring Boot already included - DevOps agents (Kubernetes, Terraform, AWS, Docker) - Testing strategies (different frameworks, visual regression) @@ -841,7 +850,7 @@ ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, comm ```bash # Install for your language(s) ./install.sh --target cursor typescript -./install.sh --target cursor python golang swift +./install.sh --target cursor python golang swift perl ``` ### What's Included @@ -850,7 +859,7 @@ ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, comm |-----------|-------|---------| | Hook Events | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt, and 10 more | | Hook Scripts | 16 | Thin Node.js scripts delegating to `scripts/hooks/` via shared adapter | -| Rules | 29 | 9 common (alwaysApply) + 20 language-specific (TypeScript, Python, Go, Swift) | +| Rules | 34 | 9 common (alwaysApply) + 25 language-specific (TypeScript, Python, Go, Swift, Perl) | | Agents | Shared | Via AGENTS.md at root (read by Cursor natively) | | Skills | Shared + Bundled | Via AGENTS.md at root and `.cursor/skills/` for translated additions | | Commands | Shared | `.cursor/commands/` if installed | @@ -1070,7 +1079,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e | **Skills** | 65 | Shared | 10 (native format) | 37 | | **Hook Events** | 8 types | 15 types | None yet | 11 types | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | -| **Rules** | 29 (common + lang) | 29 (YAML frontmatter) | Instruction-based | 13 instructions | +| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | | **Custom Tools** | Via hooks | Via hooks | N/A | 6 native tools | | **MCP Servers** | 14 | Shared (mcp.json) | 4 (command-based) | Full | | **Config Format** | settings.json | hooks.json + rules/ | config.toml | opencode.json | diff --git a/README.zh-CN.md b/README.zh-CN.md index ef72d195e..2eb08d7ad 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,6 +5,7 @@ ![Shell](https://img.shields.io/badge/-Shell-4EAA25?logo=gnu-bash&logoColor=white) ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?logo=typescript&logoColor=white) ![Go](https://img.shields.io/badge/-Go-00ADD8?logo=go&logoColor=white) +![Perl](https://img.shields.io/badge/-Perl-39457E?logo=perl&logoColor=white) ![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown&logoColor=white) --- @@ -81,8 +82,12 @@ # 首先克隆仓库 git clone https://github.com/affaan-m/everything-claude-code.git -# 复制规则(应用于所有项目) -cp -r everything-claude-code/rules/* ~/.claude/rules/ +# 复制规则(通用 + 语言特定) +cp -r everything-claude-code/rules/common/* ~/.claude/rules/ +cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择你的技术栈 +cp -r everything-claude-code/rules/python/* ~/.claude/rules/ +cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ +cp -r everything-claude-code/rules/perl/* ~/.claude/rules/ ``` ### 第三步:开始使用 @@ -175,6 +180,9 @@ everything-claude-code/ | |-- golang-patterns/ # Go 惯用语和最佳实践(新增) | |-- golang-testing/ # Go 测试模式、TDD、基准测试(新增) | |-- cpp-testing/ # C++ 测试模式、GoogleTest、CMake/CTest(新增) +| |-- perl-patterns/ # 现代 Perl 5.36+ 惯用语和最佳实践(新增) +| |-- perl-security/ # Perl 安全模式、污染模式、安全 I/O(新增) +| |-- perl-testing/ # 使用 Test2::V0、prove、Devel::Cover 的 Perl TDD(新增) | |-- commands/ # 用于快速执行的斜杠命令 | |-- tdd.md # /tdd - 测试驱动开发 @@ -197,12 +205,20 @@ everything-claude-code/ | |-- evolve.md # /evolve - 将直觉聚类到技能中(新增) | |-- rules/ # 始终遵循的指南(复制到 ~/.claude/rules/) -| |-- security.md # 强制性安全检查 -| |-- coding-style.md # 不可变性、文件组织 -| |-- testing.md # TDD、80% 覆盖率要求 -| |-- git-workflow.md # 提交格式、PR 流程 -| |-- agents.md # 何时委托给子代理 -| |-- performance.md # 模型选择、上下文管理 +| |-- README.md # 结构概述和安装指南 +| |-- common/ # 与语言无关的原则 +| | |-- coding-style.md # 不可变性、文件组织 +| | |-- git-workflow.md # 提交格式、PR 流程 +| | |-- testing.md # TDD、80% 覆盖率要求 +| | |-- performance.md # 模型选择、上下文管理 +| | |-- patterns.md # 设计模式、骨架项目 +| | |-- hooks.md # 钩子架构、TodoWrite +| | |-- agents.md # 何时委托给子代理 +| | |-- security.md # 强制性安全检查 +| |-- typescript/ # TypeScript/JavaScript 特定 +| |-- python/ # Python 特定 +| |-- golang/ # Go 特定 +| |-- perl/ # Perl 特定(新增) | |-- hooks/ # 基于触发器的自动化 | |-- hooks.json # 所有钩子配置(PreToolUse、PostToolUse、Stop 等) @@ -356,8 +372,12 @@ git clone https://github.com/affaan-m/everything-claude-code.git # 将代理复制到你的 Claude 配置 cp everything-claude-code/agents/*.md ~/.claude/agents/ -# 复制规则 -cp everything-claude-code/rules/*.md ~/.claude/rules/ +# 复制规则(通用 + 语言特定) +cp -r everything-claude-code/rules/common/* ~/.claude/rules/ +cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择你的技术栈 +cp -r everything-claude-code/rules/python/* ~/.claude/rules/ +cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ +cp -r everything-claude-code/rules/perl/* ~/.claude/rules/ # 复制命令 cp everything-claude-code/commands/*.md ~/.claude/commands/ @@ -425,13 +445,15 @@ model: opus ### 规则 -规则是始终遵循的指南。保持模块化: +规则是始终遵循的指南,分为 `common/`(通用)+ 语言特定目录: ``` ~/.claude/rules/ - security.md # 无硬编码秘密 - coding-style.md # 不可变性、文件限制 - testing.md # TDD、覆盖率要求 + common/ # 通用原则(必装) + typescript/ # TS/JS 特定模式和工具 + python/ # Python 特定模式和工具 + golang/ # Go 特定模式和工具 + perl/ # Perl 特定模式和工具 ``` --- @@ -466,7 +488,7 @@ node tests/hooks/hooks.test.js ### 贡献想法 -- 特定语言的技能(Python、Rust 模式)- 现已包含 Go! +- 特定语言的技能(Rust、C#、Kotlin、Java)- 现已包含 Go、Python、Perl、Swift 和 TypeScript! - 特定框架的配置(Django、Rails、Laravel) - DevOps 代理(Kubernetes、Terraform、AWS) - 测试策略(不同框架) diff --git a/rules/README.md b/rules/README.md index 0cb014852..a9c837765 100644 --- a/rules/README.md +++ b/rules/README.md @@ -17,7 +17,8 @@ rules/ ├── typescript/ # TypeScript/JavaScript specific ├── python/ # Python specific ├── golang/ # Go specific -└── swift/ # Swift specific +├── swift/ # Swift specific +└── perl/ # Perl specific ``` - **common/** contains universal principles — no language-specific code examples. @@ -33,6 +34,7 @@ rules/ ./install.sh python ./install.sh golang ./install.sh swift +./install.sh perl # Install multiple languages at once ./install.sh typescript python @@ -55,6 +57,7 @@ cp -r rules/typescript ~/.claude/rules/typescript cp -r rules/python ~/.claude/rules/python cp -r rules/golang ~/.claude/rules/golang cp -r rules/swift ~/.claude/rules/swift +cp -r rules/perl ~/.claude/rules/perl # Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only. ``` @@ -88,7 +91,7 @@ To add support for a new language (e.g., `rust/`): When language-specific rules and common rules conflict, **language-specific rules take precedence** (specific overrides general). This follows the standard layered configuration pattern (similar to CSS specificity or `.gitignore` precedence). - `rules/common/` defines universal defaults applicable to all projects. -- `rules/golang/`, `rules/python/`, `rules/typescript/`, etc. override those defaults where language idioms differ. +- `rules/golang/`, `rules/python/`, `rules/perl/`, `rules/typescript/`, etc. override those defaults where language idioms differ. ### Example diff --git a/rules/perl/coding-style.md b/rules/perl/coding-style.md new file mode 100644 index 000000000..9c7fbb2ff --- /dev/null +++ b/rules/perl/coding-style.md @@ -0,0 +1,46 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- +# Perl Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with Perl-specific content. + +## Standards + +- Always `use v5.36` (enables `strict`, `warnings`, `say`, subroutine signatures) +- Use subroutine signatures — never unpack `@_` manually +- Prefer `say` over `print` with explicit newlines + +## Immutability + +- Use **Moo** with `is => 'ro'` and `Types::Standard` for all attributes +- Never use blessed hashrefs directly — always use Moo/Moose accessors +- **OO override note**: Moo `has` attributes with `builder` or `default` are acceptable for computed read-only values + +## Formatting + +Use **perltidy** with these settings: + +``` +-i=4 # 4-space indent +-l=100 # 100 char line length +-ce # cuddled else +-bar # opening brace always right +``` + +## Linting + +Use **perlcritic** at severity 3 with themes: `core`, `pbp`, `security`. + +```bash +perlcritic --severity 3 --theme 'core || pbp || security' lib/ +``` + +## Reference + +See skill: `perl-patterns` for comprehensive modern Perl idioms and best practices. diff --git a/rules/perl/hooks.md b/rules/perl/hooks.md new file mode 100644 index 000000000..0b6daaddb --- /dev/null +++ b/rules/perl/hooks.md @@ -0,0 +1,22 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- +# Perl Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with Perl-specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **perltidy**: Auto-format `.pl` and `.pm` files after edit +- **perlcritic**: Run lint check after editing `.pm` files + +## Warnings + +- Warn about `print` in non-script `.pm` files — use `say` or a logging module (e.g., `Log::Any`) diff --git a/rules/perl/patterns.md b/rules/perl/patterns.md new file mode 100644 index 000000000..a2f7b4f6a --- /dev/null +++ b/rules/perl/patterns.md @@ -0,0 +1,76 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- +# Perl Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with Perl-specific content. + +## Repository Pattern + +Use **DBI** or **DBIx::Class** behind an interface: + +```perl +package MyApp::Repo::User; +use Moo; + +has dbh => (is => 'ro', required => 1); + +sub find_by_id ($self, $id) { + my $sth = $self->dbh->prepare('SELECT * FROM users WHERE id = ?'); + $sth->execute($id); + return $sth->fetchrow_hashref; +} +``` + +## DTOs / Value Objects + +Use **Moo** classes with **Types::Standard** (equivalent to Python dataclasses): + +```perl +package MyApp::DTO::User; +use Moo; +use Types::Standard qw(Str Int); + +has name => (is => 'ro', isa => Str, required => 1); +has email => (is => 'ro', isa => Str, required => 1); +has age => (is => 'ro', isa => Int); +``` + +## Resource Management + +- Always use **three-arg open** with `autodie` +- Use **Path::Tiny** for file operations + +```perl +use autodie; +use Path::Tiny; + +my $content = path('config.json')->slurp_utf8; +``` + +## Module Interface + +Use `Exporter 'import'` with `@EXPORT_OK` — never `@EXPORT`: + +```perl +use Exporter 'import'; +our @EXPORT_OK = qw(parse_config validate_input); +``` + +## Dependency Management + +Use **cpanfile** + **carton** for reproducible installs: + +```bash +carton install +carton exec prove -lr t/ +``` + +## Reference + +See skill: `perl-patterns` for comprehensive modern Perl patterns and idioms. diff --git a/rules/perl/security.md b/rules/perl/security.md new file mode 100644 index 000000000..c87fefca8 --- /dev/null +++ b/rules/perl/security.md @@ -0,0 +1,69 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- +# Perl Security + +> This file extends [common/security.md](../common/security.md) with Perl-specific content. + +## Taint Mode + +- Use `-T` flag on all CGI/web-facing scripts +- Sanitize `%ENV` (`$ENV{PATH}`, `$ENV{CDPATH}`, etc.) before any external command + +## Input Validation + +- Use allowlist regex for untainting — never `/(.*)/s` +- Validate all user input with explicit patterns: + +```perl +if ($input =~ /\A([a-zA-Z0-9_-]+)\z/) { + my $clean = $1; +} +``` + +## File I/O + +- **Three-arg open only** — never two-arg open +- Prevent path traversal with `Cwd::realpath`: + +```perl +use Cwd 'realpath'; +my $safe_path = realpath($user_path); +die "Path traversal" unless $safe_path =~ m{\A/allowed/directory/}; +``` + +## Process Execution + +- Use **list-form `system()`** — never single-string form +- Use **IPC::Run3** for capturing output +- Never use backticks with variable interpolation + +```perl +system('grep', '-r', $pattern, $directory); # safe +``` + +## SQL Injection Prevention + +Always use DBI placeholders — never interpolate into SQL: + +```perl +my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?'); +$sth->execute($email); +``` + +## Security Scanning + +Run **perlcritic** with the security theme at severity 4+: + +```bash +perlcritic --severity 4 --theme security lib/ +``` + +## Reference + +See skill: `perl-security` for comprehensive Perl security patterns, taint mode, and safe I/O. diff --git a/rules/perl/testing.md b/rules/perl/testing.md new file mode 100644 index 000000000..d451699b0 --- /dev/null +++ b/rules/perl/testing.md @@ -0,0 +1,54 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- +# Perl Testing + +> This file extends [common/testing.md](../common/testing.md) with Perl-specific content. + +## Framework + +Use **Test2::V0** for new projects (not Test::More): + +```perl +use Test2::V0; + +is($result, 42, 'answer is correct'); + +done_testing; +``` + +## Runner + +```bash +prove -l t/ # adds lib/ to @INC +prove -lr -j8 t/ # recursive, 8 parallel jobs +``` + +Always use `-l` to ensure `lib/` is on `@INC`. + +## Coverage + +Use **Devel::Cover** — target 80%+: + +```bash +cover -test +``` + +## Mocking + +- **Test::MockModule** — mock methods on existing modules +- **Test::MockObject** — create test doubles from scratch + +## Pitfalls + +- Always end test files with `done_testing` +- Never forget the `-l` flag with `prove` + +## Reference + +See skill: `perl-testing` for detailed Perl TDD patterns with Test2::V0, prove, and Devel::Cover. diff --git a/skills/perl-patterns/SKILL.md b/skills/perl-patterns/SKILL.md new file mode 100644 index 000000000..c08d18263 --- /dev/null +++ b/skills/perl-patterns/SKILL.md @@ -0,0 +1,504 @@ +--- +name: perl-patterns +description: Modern Perl 5.36+ idioms, best practices, and conventions for building robust, maintainable Perl applications. +origin: ECC +--- + +# Modern Perl Development Patterns + +Idiomatic Perl 5.36+ patterns and best practices for building robust, maintainable applications. + +## When to Activate + +- Writing new Perl code or modules +- Reviewing Perl code for idiom compliance +- Refactoring legacy Perl to modern standards +- Designing Perl module architecture +- Migrating pre-5.36 code to modern Perl + +## How It Works + +Apply these patterns as a bias toward modern Perl 5.36+ defaults: signatures, explicit modules, focused error handling, and testable boundaries. The examples below are meant to be copied as starting points, then tightened for the actual app, dependency stack, and deployment model in front of you. + +## Core Principles + +### 1. Use `v5.36` Pragma + +A single `use v5.36` replaces the old boilerplate and enables strict, warnings, and subroutine signatures. + +```perl +# Good: Modern preamble +use v5.36; + +sub greet($name) { + say "Hello, $name!"; +} + +# Bad: Legacy boilerplate +use strict; +use warnings; +use feature 'say', 'signatures'; +no warnings 'experimental::signatures'; + +sub greet { + my ($name) = @_; + say "Hello, $name!"; +} +``` + +### 2. Subroutine Signatures + +Use signatures for clarity and automatic arity checking. + +```perl +use v5.36; + +# Good: Signatures with defaults +sub connect_db($host, $port = 5432, $timeout = 30) { + # $host is required, others have defaults + return DBI->connect("dbi:Pg:host=$host;port=$port", undef, undef, { + RaiseError => 1, + PrintError => 0, + }); +} + +# Good: Slurpy parameter for variable args +sub log_message($level, @details) { + say "[$level] " . join(' ', @details); +} + +# Bad: Manual argument unpacking +sub connect_db { + my ($host, $port, $timeout) = @_; + $port //= 5432; + $timeout //= 30; + # ... +} +``` + +### 3. Context Sensitivity + +Understand scalar vs list context — a core Perl concept. + +```perl +use v5.36; + +my @items = (1, 2, 3, 4, 5); + +my @copy = @items; # List context: all elements +my $count = @items; # Scalar context: count (5) +say "Items: " . scalar @items; # Force scalar context +``` + +### 4. Postfix Dereferencing + +Use postfix dereference syntax for readability with nested structures. + +```perl +use v5.36; + +my $data = { + users => [ + { name => 'Alice', roles => ['admin', 'user'] }, + { name => 'Bob', roles => ['user'] }, + ], +}; + +# Good: Postfix dereferencing +my @users = $data->{users}->@*; +my @roles = $data->{users}[0]{roles}->@*; +my %first = $data->{users}[0]->%*; + +# Bad: Circumfix dereferencing (harder to read in chains) +my @users = @{ $data->{users} }; +my @roles = @{ $data->{users}[0]{roles} }; +``` + +### 5. The `isa` Operator (5.32+) + +Infix type-check — replaces `blessed($o) && $o->isa('X')`. + +```perl +use v5.36; +if ($obj isa 'My::Class') { $obj->do_something } +``` + +## Error Handling + +### eval/die Pattern + +```perl +use v5.36; + +sub parse_config($path) { + my $content = eval { path($path)->slurp_utf8 }; + die "Config error: $@" if $@; + return decode_json($content); +} +``` + +### Try::Tiny (Reliable Exception Handling) + +```perl +use v5.36; +use Try::Tiny; + +sub fetch_user($id) { + my $user = try { + $db->resultset('User')->find($id) + // die "User $id not found\n"; + } + catch { + warn "Failed to fetch user $id: $_"; + undef; + }; + return $user; +} +``` + +### Native try/catch (5.40+) + +```perl +use v5.40; + +sub divide($x, $y) { + try { + die "Division by zero" if $y == 0; + return $x / $y; + } + catch ($e) { + warn "Error: $e"; + return; + } +} +``` + +## Modern OO with Moo + +Prefer Moo for lightweight, modern OO. Use Moose only when its metaprotocol is needed. + +```perl +# Good: Moo class +package User; +use Moo; +use Types::Standard qw(Str Int ArrayRef); +use namespace::autoclean; + +has name => (is => 'ro', isa => Str, required => 1); +has email => (is => 'ro', isa => Str, required => 1); +has age => (is => 'ro', isa => Int, default => sub { 0 }); +has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] }); + +sub is_admin($self) { + return grep { $_ eq 'admin' } $self->roles->@*; +} + +sub greet($self) { + return "Hello, I'm " . $self->name; +} + +1; + +# Usage +my $user = User->new( + name => 'Alice', + email => 'alice@example.com', + roles => ['admin', 'user'], +); + +# Bad: Blessed hashref (no validation, no accessors) +package User; +sub new { + my ($class, %args) = @_; + return bless \%args, $class; +} +sub name { return $_[0]->{name} } +1; +``` + +### Moo Roles + +```perl +package Role::Serializable; +use Moo::Role; +use JSON::MaybeXS qw(encode_json); +requires 'TO_HASH'; +sub to_json($self) { encode_json($self->TO_HASH) } +1; + +package User; +use Moo; +with 'Role::Serializable'; +has name => (is => 'ro', required => 1); +has email => (is => 'ro', required => 1); +sub TO_HASH($self) { { name => $self->name, email => $self->email } } +1; +``` + +### Native `class` Keyword (5.38+, Corinna) + +```perl +use v5.38; +use feature 'class'; +no warnings 'experimental::class'; + +class Point { + field $x :param; + field $y :param; + method magnitude() { sqrt($x**2 + $y**2) } +} + +my $p = Point->new(x => 3, y => 4); +say $p->magnitude; # 5 +``` + +## Regular Expressions + +### Named Captures and `/x` Flag + +```perl +use v5.36; + +# Good: Named captures with /x for readability +my $log_re = qr{ + ^ (? \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} ) + \s+ \[ (? \w+ ) \] + \s+ (? .+ ) $ +}x; + +if ($line =~ $log_re) { + say "Time: $+{timestamp}, Level: $+{level}"; + say "Message: $+{message}"; +} + +# Bad: Positional captures (hard to maintain) +if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) { + say "Time: $1, Level: $2"; +} +``` + +### Precompiled Patterns + +```perl +use v5.36; + +# Good: Compile once, use many +my $email_re = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; + +sub validate_emails(@emails) { + return grep { $_ =~ $email_re } @emails; +} +``` + +## Data Structures + +### References and Safe Deep Access + +```perl +use v5.36; + +# Hash and array references +my $config = { + database => { + host => 'localhost', + port => 5432, + options => ['utf8', 'sslmode=require'], + }, +}; + +# Safe deep access (returns undef if any level missing) +my $port = $config->{database}{port}; # 5432 +my $missing = $config->{cache}{host}; # undef, no error + +# Hash slices +my %subset; +@subset{qw(host port)} = @{$config->{database}}{qw(host port)}; + +# Array slices +my @first_two = $config->{database}{options}->@[0, 1]; + +# Multi-variable for loop (experimental in 5.36, stable in 5.40) +use feature 'for_list'; +no warnings 'experimental::for_list'; +for my ($key, $val) (%$config) { + say "$key => $val"; +} +``` + +## File I/O + +### Three-Argument Open + +```perl +use v5.36; + +# Good: Three-arg open with autodie (core module, eliminates 'or die') +use autodie; + +sub read_file($path) { + open my $fh, '<:encoding(UTF-8)', $path; + local $/; + my $content = <$fh>; + close $fh; + return $content; +} + +# Bad: Two-arg open (shell injection risk, see perl-security) +open FH, $path; # NEVER do this +open FH, "< $path"; # Still bad — user data in mode string +``` + +### Path::Tiny for File Operations + +```perl +use v5.36; +use Path::Tiny; + +my $file = path('config', 'app.json'); +my $content = $file->slurp_utf8; +$file->spew_utf8($new_content); + +# Iterate directory +for my $child (path('src')->children(qr/\.pl$/)) { + say $child->basename; +} +``` + +## Module Organization + +### Standard Project Layout + +```text +MyApp/ +├── lib/ +│ └── MyApp/ +│ ├── App.pm # Main module +│ ├── Config.pm # Configuration +│ ├── DB.pm # Database layer +│ └── Util.pm # Utilities +├── bin/ +│ └── myapp # Entry-point script +├── t/ +│ ├── 00-load.t # Compilation tests +│ ├── unit/ # Unit tests +│ └── integration/ # Integration tests +├── cpanfile # Dependencies +├── Makefile.PL # Build system +└── .perlcriticrc # Linting config +``` + +### Exporter Patterns + +```perl +package MyApp::Util; +use v5.36; +use Exporter 'import'; + +our @EXPORT_OK = qw(trim); +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +sub trim($str) { $str =~ s/^\s+|\s+$//gr } + +1; +``` + +## Tooling + +### perltidy Configuration (.perltidyrc) + +```text +-i=4 # 4-space indent +-l=100 # 100-char line length +-ci=4 # continuation indent +-ce # cuddled else +-bar # opening brace on same line +-nolq # don't outdent long quoted strings +``` + +### perlcritic Configuration (.perlcriticrc) + +```ini +severity = 3 +theme = core + pbp + security + +[InputOutput::RequireCheckedSyscalls] +functions = :builtins +exclude_functions = say print + +[Subroutines::ProhibitExplicitReturnUndef] +severity = 4 + +[ValuesAndExpressions::ProhibitMagicNumbers] +allowed_values = 0 1 2 -1 +``` + +### Dependency Management (cpanfile + carton) + +```bash +cpanm App::cpanminus Carton # Install tools +carton install # Install deps from cpanfile +carton exec -- perl bin/myapp # Run with local deps +``` + +```perl +# cpanfile +requires 'Moo', '>= 2.005'; +requires 'Path::Tiny'; +requires 'JSON::MaybeXS'; +requires 'Try::Tiny'; + +on test => sub { + requires 'Test2::V0'; + requires 'Test::MockModule'; +}; +``` + +## Quick Reference: Modern Perl Idioms + +| Legacy Pattern | Modern Replacement | +|---|---| +| `use strict; use warnings;` | `use v5.36;` | +| `my ($x, $y) = @_;` | `sub foo($x, $y) { ... }` | +| `@{ $ref }` | `$ref->@*` | +| `%{ $ref }` | `$ref->%*` | +| `open FH, "< $file"` | `open my $fh, '<:encoding(UTF-8)', $file` | +| `blessed hashref` | `Moo` class with types | +| `$1, $2, $3` | `$+{name}` (named captures) | +| `eval { }; if ($@)` | `Try::Tiny` or native `try/catch` (5.40+) | +| `BEGIN { require Exporter; }` | `use Exporter 'import';` | +| Manual file ops | `Path::Tiny` | +| `blessed($o) && $o->isa('X')` | `$o isa 'X'` (5.32+) | +| `builtin::true / false` | `use builtin 'true', 'false';` (5.36+, experimental) | + +## Anti-Patterns + +```perl +# 1. Two-arg open (security risk) +open FH, $filename; # NEVER + +# 2. Indirect object syntax (ambiguous parsing) +my $obj = new Foo(bar => 1); # Bad +my $obj = Foo->new(bar => 1); # Good + +# 3. Excessive reliance on $_ +map { process($_) } grep { validate($_) } @items; # Hard to follow +my @valid = grep { validate($_) } @items; # Better: break it up +my @results = map { process($_) } @valid; + +# 4. Disabling strict refs +no strict 'refs'; # Almost always wrong +${"My::Package::$var"} = $value; # Use a hash instead + +# 5. Global variables as configuration +our $TIMEOUT = 30; # Bad: mutable global +use constant TIMEOUT => 30; # Better: constant +# Best: Moo attribute with default + +# 6. String eval for module loading +eval "require $module"; # Bad: code injection risk +eval "use $module"; # Bad +use Module::Runtime 'require_module'; # Good: safe module loading +require_module($module); +``` + +**Remember**: Modern Perl is clean, readable, and safe. Let `use v5.36` handle the boilerplate, use Moo for objects, and prefer CPAN's battle-tested modules over hand-rolled solutions. diff --git a/skills/perl-security/SKILL.md b/skills/perl-security/SKILL.md new file mode 100644 index 000000000..679c577d2 --- /dev/null +++ b/skills/perl-security/SKILL.md @@ -0,0 +1,503 @@ +--- +name: perl-security +description: Comprehensive Perl security covering taint mode, input validation, safe process execution, DBI parameterized queries, web security (XSS/SQLi/CSRF), and perlcritic security policies. +origin: ECC +--- + +# Perl Security Patterns + +Comprehensive security guidelines for Perl applications covering input validation, injection prevention, and secure coding practices. + +## When to Activate + +- Handling user input in Perl applications +- Building Perl web applications (CGI, Mojolicious, Dancer2, Catalyst) +- Reviewing Perl code for security vulnerabilities +- Performing file operations with user-supplied paths +- Executing system commands from Perl +- Writing DBI database queries + +## How It Works + +Start with taint-aware input boundaries, then move outward: validate and untaint inputs, keep filesystem and process execution constrained, and use parameterized DBI queries everywhere. The examples below show the safe defaults this skill expects you to apply before shipping Perl code that touches user input, the shell, or the network. + +## Taint Mode + +Perl's taint mode (`-T`) tracks data from external sources and prevents it from being used in unsafe operations without explicit validation. + +### Enabling Taint Mode + +```perl +#!/usr/bin/perl -T +use v5.36; + +# Tainted: anything from outside the program +my $input = $ARGV[0]; # Tainted +my $env_path = $ENV{PATH}; # Tainted +my $form = ; # Tainted +my $query = $ENV{QUERY_STRING}; # Tainted + +# Sanitize PATH early (required in taint mode) +$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin'; +delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; +``` + +### Untainting Pattern + +```perl +use v5.36; + +# Good: Validate and untaint with a specific regex +sub untaint_username($input) { + if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) { + return $1; # $1 is untainted + } + die "Invalid username: must be 3-30 alphanumeric characters\n"; +} + +# Good: Validate and untaint a file path +sub untaint_filename($input) { + if ($input =~ m{^([a-zA-Z0-9._-]+)$}) { + return $1; + } + die "Invalid filename: contains unsafe characters\n"; +} + +# Bad: Overly permissive untainting (defeats the purpose) +sub bad_untaint($input) { + $input =~ /^(.*)$/s; + return $1; # Accepts ANYTHING — pointless +} +``` + +## Input Validation + +### Allowlist Over Blocklist + +```perl +use v5.36; + +# Good: Allowlist — define exactly what's permitted +sub validate_sort_field($field) { + my %allowed = map { $_ => 1 } qw(name email created_at updated_at); + die "Invalid sort field: $field\n" unless $allowed{$field}; + return $field; +} + +# Good: Validate with specific patterns +sub validate_email($email) { + if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) { + return $1; + } + die "Invalid email address\n"; +} + +sub validate_integer($input) { + if ($input =~ /^(-?\d{1,10})$/) { + return $1 + 0; # Coerce to number + } + die "Invalid integer\n"; +} + +# Bad: Blocklist — always incomplete +sub bad_validate($input) { + die "Invalid" if $input =~ /[<>"';&|]/; # Misses encoded attacks + return $input; +} +``` + +### Length Constraints + +```perl +use v5.36; + +sub validate_comment($text) { + die "Comment is required\n" unless length($text) > 0; + die "Comment exceeds 10000 chars\n" if length($text) > 10_000; + return $text; +} +``` + +## Safe Regular Expressions + +### ReDoS Prevention + +Catastrophic backtracking occurs with nested quantifiers on overlapping patterns. + +```perl +use v5.36; + +# Bad: Vulnerable to ReDoS (exponential backtracking) +my $bad_re = qr/^(a+)+$/; # Nested quantifiers +my $bad_re2 = qr/^([a-zA-Z]+)*$/; # Nested quantifiers on class +my $bad_re3 = qr/^(.*?,){10,}$/; # Repeated greedy/lazy combo + +# Good: Rewrite without nesting +my $good_re = qr/^a+$/; # Single quantifier +my $good_re2 = qr/^[a-zA-Z]+$/; # Single quantifier on class + +# Good: Use possessive quantifiers or atomic groups to prevent backtracking +my $safe_re = qr/^[a-zA-Z]++$/; # Possessive (5.10+) +my $safe_re2 = qr/^(?>a+)$/; # Atomic group + +# Good: Enforce timeout on untrusted patterns +use POSIX qw(alarm); +sub safe_match($string, $pattern, $timeout = 2) { + my $matched; + eval { + local $SIG{ALRM} = sub { die "Regex timeout\n" }; + alarm($timeout); + $matched = $string =~ $pattern; + alarm(0); + }; + alarm(0); + die $@ if $@; + return $matched; +} +``` + +## Safe File Operations + +### Three-Argument Open + +```perl +use v5.36; + +# Good: Three-arg open, lexical filehandle, check return +sub read_file($path) { + open my $fh, '<:encoding(UTF-8)', $path + or die "Cannot open '$path': $!\n"; + local $/; + my $content = <$fh>; + close $fh; + return $content; +} + +# Bad: Two-arg open with user data (command injection) +sub bad_read($path) { + open my $fh, $path; # If $path = "|rm -rf /", runs command! + open my $fh, "< $path"; # Shell metacharacter injection +} +``` + +### TOCTOU Prevention and Path Traversal + +```perl +use v5.36; +use Fcntl qw(:DEFAULT :flock); +use File::Spec; +use Cwd qw(realpath); + +# Atomic file creation +sub create_file_safe($path) { + sysopen(my $fh, $path, O_WRONLY | O_CREAT | O_EXCL, 0600) + or die "Cannot create '$path': $!\n"; + return $fh; +} + +# Validate path stays within allowed directory +sub safe_path($base_dir, $user_path) { + my $real = realpath(File::Spec->catfile($base_dir, $user_path)) + // die "Path does not exist\n"; + my $base_real = realpath($base_dir) + // die "Base dir does not exist\n"; + die "Path traversal blocked\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/; + return $real; +} +``` + +Use `File::Temp` for temporary files (`tempfile(UNLINK => 1)`) and `flock(LOCK_EX)` to prevent race conditions. + +## Safe Process Execution + +### List-Form system and exec + +```perl +use v5.36; + +# Good: List form — no shell interpolation +sub run_command(@cmd) { + system(@cmd) == 0 + or die "Command failed: @cmd\n"; +} + +run_command('grep', '-r', $user_pattern, '/var/log/app/'); + +# Good: Capture output safely with IPC::Run3 +use IPC::Run3; +sub capture_output(@cmd) { + my ($stdout, $stderr); + run3(\@cmd, \undef, \$stdout, \$stderr); + if ($?) { + die "Command failed (exit $?): $stderr\n"; + } + return $stdout; +} + +# Bad: String form — shell injection! +sub bad_search($pattern) { + system("grep -r '$pattern' /var/log/app/"); # If $pattern = "'; rm -rf / #" +} + +# Bad: Backticks with interpolation +my $output = `ls $user_dir`; # Shell injection risk +``` + +Also use `Capture::Tiny` for capturing stdout/stderr from external commands safely. + +## SQL Injection Prevention + +### DBI Placeholders + +```perl +use v5.36; +use DBI; + +my $dbh = DBI->connect($dsn, $user, $pass, { + RaiseError => 1, + PrintError => 0, + AutoCommit => 1, +}); + +# Good: Parameterized queries — always use placeholders +sub find_user($dbh, $email) { + my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?'); + $sth->execute($email); + return $sth->fetchrow_hashref; +} + +sub search_users($dbh, $name, $status) { + my $sth = $dbh->prepare( + 'SELECT * FROM users WHERE name LIKE ? AND status = ? ORDER BY name' + ); + $sth->execute("%$name%", $status); + return $sth->fetchall_arrayref({}); +} + +# Bad: String interpolation in SQL (SQLi vulnerability!) +sub bad_find($dbh, $email) { + my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'"); + # If $email = "' OR 1=1 --", returns all users + $sth->execute; + return $sth->fetchrow_hashref; +} +``` + +### Dynamic Column Allowlists + +```perl +use v5.36; + +# Good: Validate column names against an allowlist +sub order_by($dbh, $column, $direction) { + my %allowed_cols = map { $_ => 1 } qw(name email created_at); + my %allowed_dirs = map { $_ => 1 } qw(ASC DESC); + + die "Invalid column: $column\n" unless $allowed_cols{$column}; + die "Invalid direction: $direction\n" unless $allowed_dirs{uc $direction}; + + my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction"); + $sth->execute; + return $sth->fetchall_arrayref({}); +} + +# Bad: Directly interpolating user-chosen column +sub bad_order($dbh, $column) { + $dbh->prepare("SELECT * FROM users ORDER BY $column"); # SQLi! +} +``` + +### DBIx::Class (ORM Safety) + +```perl +use v5.36; + +# DBIx::Class generates safe parameterized queries +my @users = $schema->resultset('User')->search({ + status => 'active', + email => { -like => '%@example.com' }, +}, { + order_by => { -asc => 'name' }, + rows => 50, +}); +``` + +## Web Security + +### XSS Prevention + +```perl +use v5.36; +use HTML::Entities qw(encode_entities); +use URI::Escape qw(uri_escape_utf8); + +# Good: Encode output for HTML context +sub safe_html($user_input) { + return encode_entities($user_input); +} + +# Good: Encode for URL context +sub safe_url_param($value) { + return uri_escape_utf8($value); +} + +# Good: Encode for JSON context +use JSON::MaybeXS qw(encode_json); +sub safe_json($data) { + return encode_json($data); # Handles escaping +} + +# Template auto-escaping (Mojolicious) +# <%= $user_input %> — auto-escaped (safe) +# <%== $raw_html %> — raw output (dangerous, use only for trusted content) + +# Template auto-escaping (Template Toolkit) +# [% user_input | html %] — explicit HTML encoding + +# Bad: Raw output in HTML +sub bad_html($input) { + print "
$input
"; # XSS if $input contains