Flaw on Fedora: Static Security and AI-Slop Analysis for Crystal
Yesterday I was auditing a batch of Crystal projects on my Fedora workstation, and it hit me how much the day-to-day code review threat model has shifted.
For years, if you wrote Crystal, your static analysis workflow was straightforward: you ran the compiler for type safety and Ameba for style and code smells. And la neta, Ameba is fantastic at what it does. It catches unused variables, cyclomatic complexity, and formatting drift before they pollute your repository.
Today, however, you face two problems that a style linter was never designed to solve:
- Classic application security sinks: Command injection via interpolated strings, SQL queries assembled with +, weak random number generators near session tokens, disabled TLS verification, and timing attacks on secret comparisons.
- Unreviewed LLM paste-through: What the industry now calls AI-slop—leftover your-api-key-here placeholders, swallowed exceptions, narration comments explaining the obvious, and the worst offender of all: commented-out authentication guards that an AI coding agent disabled just to make a failing spec pass.
That is where flaw comes in. Created by kdairatchi, flaw is a fast, zero-dependency static security and code-hygiene scanner built specifically for Crystal. Even better, I just packaged it natively for Fedora Linux inside the renich/crystal Copr repository as part of the ongoing push to bring the entire Crystal ecosystem into official Fedora and EPEL repositories.
In gemology, a flaw is an internal fracture or inclusion inside a crystal that you spot by holding the stone up to the light. Let me show you how flaw holds your Crystal codebase up to the light, how its AST and taint engine works under the hood, and how you can install it on Fedora in ten seconds flat.
Installing crystal-flaw Natively on Fedora
You do not need to clone git repositories or compile toolchains by hand on every workstation or CI runner. Both crystal-flaw and ameba are pre-built for x86_64 and aarch64 in the renich/crystal Copr repository.
Enable the repository and install crystal-flaw with dnf:
# as root
dnf -y copr enable renich/crystal
dnf -y install crystal-flaw
Warning
Make sure you install crystal-flaw and not flaw! Fedora's official repositories already ship an existing package named flaw—a top-down wizard battle game from SourceForge. Installing crystal-flaw ensures you get the Crystal static security analyzer instead of a wizard arena game.
Verify your installation right away:
# Verify installed version and rule catalog
flaw version
flaw rules
Note
Because flaw is compiled to a native binary against Fedora's system libraries such as libyaml, pcre2, and gc, startup time is practically instantaneous. You can drop it into local pre-commit hooks without adding noticeable latency to your workflow.
How flaw Works Under the Hood
When I inspected flaw's source code before packaging crystal-flaw, I appreciated how pragmatic its architecture is. Instead of forcing every check through a heavyweight compiler pass or relying purely on brittle regular expressions, flaw uses a two-tier scanning pipeline coordinated by Flaw::Scanner:
- Pre-Scan Normalization and Inline Suppressions:
- Before any rule runs on a .cr file, Flaw::SourcePrep.mask_heredocs blanks out heredoc bodies like <<-SQL or <<-YAML while preserving exact line numbers so embedded multi-line templates do not trigger false positives on comment or regex detectors. In parallel, Flaw::Suppression.parse indexes inline suppression directives: # flaw:ignore, # flaw:ignore-next, and # flaw:ignore-file.
- Tier 1 - Lexical, Windowed, and Shannon Entropy Detectors:
- Rules that subclass Flaw::Rule operate on the normalized source lines or multi-line token windows. For example, FLAW002 for hardcoded secrets combines known credential prefix patterns—AWS keys, GitHub tokens, Slack webhooks, and PEM private keys—with Shannon entropy calculation via Flaw::Entropy.shannon. Any string literal assigned to a secret-like identifier that scores at least 3.5 bits per character across six or more unique characters is flagged immediately, while low-entropy placeholders and words are ignored.
- Tier 2 - Single-Pass Crystal AST Visitor and Local Taint Analysis:
- Rules that require syntactic precision subclass Flaw::AstRule. Rather than re-parsing a file for every rule, Flaw::AstBackend parses each source file once using Crystal's official compiler/crystal/syntax parser, Crystal::Parser, and dispatches all active AST rules through a single shared Crystal::Visitor.
Tip
If a file contains incomplete syntax while you are actively editing it, Flaw::AstBackend.parse catches the parse exception and gracefully falls back to Tier 1 regex rules without aborting the scan.
Intraprocedural Bindings and Shape-Based Taint Tracking
The biggest headache with naive security scanners is false positives. If a tool yells at you every time you call Process.run or db.query, you will eventually mute the tool and move on.
flaw avoids this trap through Flaw::Analysis::BindingCollector and Flaw::Taint. Before dispatching AST rules on a parsed syntax tree, flaw walks Crystal::Assign nodes to build a per-file map of local variable bindings. When FLAW001 inspects a system, backtick, or Process.run call containing a Crystal::StringInterpolation, it traces each interpolated expression up to a recursion depth of 3 against known taint sources and sanitizers:
- Tainted Sources:
- Interpolations reading from params, request, session, cookies, headers, ARGV, ENV, or methods like query_params, path_params, form_params, and body.
- Sanitizers and Safe Coercions:
- Expressions wrapped in numeric coercions such as to_i, to_i64, and to_f, escaping helpers on URI, HTML, JSON, or Base64, and Crypto::Subtle.constant_time_compare.
Look at the difference in practice. If you pass user input directly into a shell command or SQL query, flaw catches it red-handed:
# vulnerable_example.cr
def handle_ping(host : String, db : DB::Database, id : String)
output = `ping -c1 #{host}`
db.query_one("SELECT * FROM users WHERE id = #{id}") { |rs| rs.read(String) }
end
Running flaw scan on that snippet produces pinpoint diagnostics with line, column, severity, and remediation advice:
# Output of: flaw scan vulnerable_example.cr
vulnerable_example.cr
3:11 [critical] FLAW001 OS command built from interpolated input
Command built via string interpolation — pass Process.run an argv array or taint-check the inputs
│ output = `ping -c1 #{host}`
4:15 [high] FLAW003 SQL built by string interpolation or concatenation
SQL built via interpolation — use parameterised queries
│ db.query_one("SELECT * FROM users WHERE id = #{id}") { |rs| rs.read(String) }
flaw: 2 findings (1 critical, 1 high)
Once you refactor Process.run to pass an argv array, which bypasses /bin/sh entirely, and switch db.query_one to parameterized placeholders like WHERE id = ?, the AST node changes from Crystal::StringInterpolation to Crystal::StringLiteral and flaw passes with zero noise.
The 30 Built-In Rules: Security and AI-Slop
Out of the box, flaw 0.1.0 ships with 30 rules split into two distinct namespaces, each mapped to CWE and OWASP Top 10 identifiers for compliance and SARIF reporting.
Security Rules: FLAW001 to FLAW024
The FLAW0xx series targets real vulnerabilities that should break your CI build immediately:
| Rule ID | Severity | CWE | Vulnerability Detected |
|---|---|---|---|
| FLAW001 | critical | CWE-78 | OS command built from interpolated input via system, backticks, or Process.run |
| FLAW002 | high | CWE-798 | Hardcoded secret literal via prefix match and Shannon entropy >= 3.5 |
| FLAW003 | high | CWE-89 | SQL built via string interpolation or + concatenation |
| FLAW004 | high | CWE-338 | Non-cryptographic RNG such as rand or Random.new near security-sensitive identifiers |
| FLAW005 | medium | CWE-502 | YAML.parse called on untrusted input |
| FLAW006 | high | CWE-22 | File access with user-controlled path allowing path traversal |
| FLAW007 | medium | CWE-601 | Open redirect to user-supplied URL without allowlist |
| FLAW008 | high | CWE-502 | Unsafe deserialization of untrusted data |
| FLAW009 | high | CWE-328 | Weak hash MD5/SHA1 used for passwords or integrity |
| FLAW010 | high | CWE-295 | TLS certificate verification disabled via OpenSSL::SSL::VerifyMode::NONE |
| FLAW011 | high | CWE-918 | Outbound HTTP request to user-controlled URL enabling SSRF |
| FLAW012 | medium | CWE-208 | Secret compared with == instead of Crypto::Subtle.constant_time_compare |
| FLAW013 | medium | CWE-377 | Tempfile created without atomic O_EXCL |
| FLAW014 | high | CWE-611 | XML parsed without disabling external entities, risking XXE |
| FLAW015 | medium | CWE-915 | Privilege field exposed through Serializable mass assignment |
| FLAW016 | medium | CWE-326 | TLS minimum version pinned to deprecated SSLv3, TLSv1.0, or TLSv1.1 |
| FLAW017 | low | CWE-1333 | Regex pattern with nested quantifiers, risking ReDoS |
| FLAW018 | high | CWE-532 | Secret-named value written to log or stdout |
| FLAW019 | medium | CWE-1004 | Cookie set without Secure, HttpOnly, or SameSite flags |
| FLAW020 | high | CWE-327 | ECB cipher mode used, leaking plaintext block structure |
| FLAW021 | high | CWE-329 | Hardcoded initialization vector, nonce, or salt |
| FLAW022 | high | CWE-22 | Archive entry extracted without path normalization, enabling zip-slip |
| FLAW023 | critical | CWE-347 | JWT accepted with alg: none or verification disabled |
| FLAW024 | high | CWE-942 | CORS wildcard Access-Control-Allow-Origin: * with Allow-Credentials: true |
AI-Slop and Hygiene Rules: FLAW100 to FLAW105
This is where flaw enters uncharted territory that no other Crystal linter touches. When you pair-program with AI agents or review community pull requests generated with LLMs, certain failure patterns show up over and over again. The FLAW1xx rules specifically hunt down unedited AI artifacts:
- FLAW100 - Explanatory Narration Comments, low severity:
- Flags textbook LLM narration comments like # This function does X or # Here we loop through the array.
- FLAW101 - AI Assistant Boilerplate Leaked into Source, medium severity:
- Catches conversational refusal or meta-text pasted verbatim into strings or comments, such as As an AI... or I apologize....
- FLAW102 - Unreplaced Placeholder Values, medium severity:
- Detects dummy strings left behind in configuration or client code such as your-api-key-here or REPLACE_ME.
- FLAW103 - Unfinished Stubs, medium severity, CWE-1163:
- Catches raise NotImplementedError or # TODO: implement stubs that an agent left half-baked.
- FLAW104 - Broad Swallowed Rescue Blocks, low severity, CWE-390:
- Catches rescue Exception; nil blocks that silently swallow runtime failures to fake a passing execution.
- FLAW105 - Commented-Out Authentication or Authorization Guards, high severity, CWE-862:
- Flags commented-out security checks like # before_action :authenticate_admin! or # authorize! :manage, :all.
Warning
FLAW105 is a lifesaver when working with autonomous coding agents. When an LLM struggles to mock an authentication token in a spec, its favorite shortcut is to comment out the auth guard in your controller so the test turns green. Running flaw scan --fail-on high stops that regression cold before it ever reaches code review.
Practical Workflows: Configs, Baselines, and CI Output
To configure flaw for a repository, scaffold a default .flaw.yml at the project root:
# Generate .flaw.yml configuration stub
flaw init config
You can exclude directories, disable specific rules, or override rule severities per project:
# .flaw.yml
version: 1
exclude:
- spec/
- lib/
- vendor/
rules:
FLAW002:
ignore:
- "examples/fake-keys.cr"
FLAW004:
severity: critical
Adopting flaw in an existing codebase with legacy technical debt does not require fixing fifty historical warnings on day one. Generate a baseline snapshot so CI only blocks on new findings:
# Record current findings into .flaw-baseline.json
flaw baseline --out .flaw-baseline.json
# Gate CI strictly on new medium+ findings not present in the baseline
flaw scan . --baseline .flaw-baseline.json --fail-on medium
# Emit JSON for automated agent consumption or SARIF 2.1.0 for code scanning
flaw scan src/ --format json
flaw scan . --format sarif > flaw.sarif
How I Packaged flaw for Fedora: The Shard-as-RPM Model
Packaging flaw for Fedora in crystal-flaw was a great stress-test for the Shard-as-RPM packaging macros in crystal-rpm-macros.
In Fedora, build environments like Koji and Mock run in strict network isolation. Calling shards install over the network during %build is forbidden, and bundling unvetted third-party dependencies violates Fedora policy. Under the Shard-as-RPM model, every Crystal library is packaged as its own RPM under %{_datadir}/crystal/shards/, and applications build 100% offline using declarative macros.
Look at how clean crystal-flaw.spec is using crystal-rpm-macros:
# crystal-flaw.spec
%bcond_without debug
%bcond_without check
%{!?crystal_arches: %global crystal_arches x86_64 aarch64}
%global shard_name flaw
Name: crystal-%{shard_name}
Version: 0.1.0
Release: 1%{?dist}
Summary: Static security analysis and flaw detection for Crystal
License: MIT
URL: https://github.com/kdairatchi/flaw
Source0: %{url}/archive/v%{version}/%{shard_name}-%{version}.tar.gz
ExclusiveArch: %{crystal_arches}
BuildRequires: crystal-rpm-macros
BuildRequires: crystal >= 1.21.0
BuildRequires: shards
BuildRequires: libyaml-devel
%description
Flaw is a fast static analysis tool for finding security flaws and vulnerabilities
in Crystal source code.
%prep
%crystal_setup -n %{shard_name}-%{version}
%build
mkdir -p bin
%crystal_build -o bin/%{shard_name} src/main.cr
%install
%crystal_install -b bin/%{shard_name}
%check
%if %{with check}
%crystal_check
%endif
%files
%license LICENSE
%doc README.md
%{_bindir}/%{shard_name}
%changelog
* Sun Sep 06 2026 Rénich Bon Ćirić <[email protected]> - 0.1.0-1
- Initial package for Fedora
Because Fedora already has the flaw wizard battle game in the official repositories, keeping the package name prefixed as crystal-flaw avoids a package namespace collision with Fedora's base repos.
Here is what those four macros do for you behind the scenes:
- %crystal_setup:
- Unpacks the source archive and invokes shards-link to automatically symlink any required system-installed shards from /usr/share/crystal/shards into the local lib/ directory without touching the network.
- %crystal_build:
- Exports Fedora's hardened CFLAGS and CRYSTAL_PATH, then compiles the target in --release mode with DWARF debug symbols enabled via -d and Fedora's build_ldflags injected into the linker so rpmbuild extracts proper debuginfo and debugsource packages.
- %crystal_install:
- Installs the resulting binary into %{_bindir}/flaw with 0755 permissions while preserving timestamps.
- %crystal_check:
- Executes crystal spec completely offline inside the isolated Mock chroot during the %check phase, verifying Shannon entropy calculations, heredoc masking, inline suppressions, and baseline filtering before the RPM is ever signed.
Join the Fedora Crystal SIG, Compa!
Having crystal, shards, ameba, crystal-flaw, cr2rpm, and core shards like kemal, db, pg, sqlite3, and mysql running natively from renich/crystal Copr is just the beginning. My goal is to take the Fedora Crystal SIG across the finish line and make Crystal a first-class, officially supported language in Fedora 43+ and EPEL.
If you write Crystal, maintain shards, or love Fedora packaging, I would love to have you on board as a founding contributor to the SIG. Here is how you can jump in right now:
- Test the Copr repository: Enable renich/crystal on your Fedora machine, install crystal, ameba, crystal-flaw, and cr2rpm, and put them through their paces on your own projects.
- Package your favorite shards: Grab cr2rpm with dnf -y install cr2rpm, run cr2rpm generate --path . on a Crystal shard, and help expand the Shard-as-RPM dependency tree on Codeberg.
- Contribute upstream to flaw: flaw makes it trivially easy to add new security detectors—run flaw init rule FLAW025 my-new-rule, write your bad.cr and good.cr fixtures, and validate the contract with flaw lint-rules and flaw doctor.
- Join the SIG conversation: Drop by the Fedora Discussion thread and check out the planning and packaging repositories on Codeberg.
Together, you and I can make Fedora the best Linux distribution on the planet for Crystal developers, no?! ;)