COR-1861: Port SQL language support from the 0.1.2 fork onto main - #72
Open
juangaitanv wants to merge 4 commits into
Open
COR-1861: Port SQL language support from the 0.1.2 fork onto main#72juangaitanv wants to merge 4 commits into
juangaitanv wants to merge 4 commits into
Conversation
SQL support already ships in production as sighthound 0.1.2, built from commit 6dfc357, which is not an ancestor of main. main is 0.1.1 and never had rules/sql. This is unported work, not a regression, so this change is a port rather than new development. Wiring (8 sites): sql feature + default, SQLLanguage registry arm and impl, the "sql" | "ddl" | "dml" detection arm, the should_include_file arm, the embedded rules dir and its load_embedded_rules arm, and the [LANGUAGE] help string. Two deliberate divergences from the production rule pack, both false-positive downgrades that keep the detection: - sql-injection-001 fired Critical on a bare "||", so ordinary column concatenation read as Critical SQL Injection. "||" moves to a new sql-injection-002 at Low/Low; the original keeps its three globs at Critical. - sql-comment-001 reported an ordinary trailing -- comment as "SQL Injection" (Medium). It is now Code Smell / Low. Relax the combined-mode taint guard. Fusion invokes the binary as `sighthound --output-format json <file>` — default combined mode — and all 11 SQL rules are mode: "search", so run_taint_analysis_with_verbosity hard-errored with "No taint flow rules found" on the only invocation that matters. With no taint rules and verbose_mode = false the taint pass now returns Ok(Vec::new()) instead of erroring; run_selected_analysis appends that result to the simple pass, so it must be empty rather than a search-pass re-run, which would double-count. An explicit --taint-analysis still errors. Blast radius is rule sets with zero taint rules, which previously always failed, so this can only turn errors into results; ObjectScript-only scans now succeed for the same reason. Do not bump the version: fusion/Dockerfile:40 hard-fails unless the binary reports exactly 0.1.2, and that pin is the only thing stopping a re-vendor from main from silently dropping the fork-only languages. Document the limitations honestly in README.md — no SQL grammar, no dataflow, byte-0-anchored globs, end_line always EOF, and .ddl/.dml detected but never reported because every rule is scoped to .sql. Tests: registry/extension/rule-pack pins, an unsafe+safe fixture pair, a ddl/dml zero-finding pin, and both halves of the taint-guard branch. make ci green, 263 tests.
juangaitanv
requested review from
Ibrahimrahhal,
asadeddin,
leenk7991 and
yhoztak
August 17, 2026 13:48
leenk7991
requested changes
Aug 20, 2026
SQL matches the whole file as one node, so findings carried the entire file as snippet/function/sink function_name and file-end as end_line. Clamp the emitted finding to the matched line in both the simple and enhanced-search paths; detection stays whole-file. Claude-Session: https://claude.ai/code/session_011Kr7vyvRnTDSQtVUeigd1c
leenk7991
reviewed
Aug 24, 2026
leenk7991
left a comment
Member
There was a problem hiding this comment.
i think we should also add "sql" to the bakend type in detect_code_type
can we also make sure that SIGHTHOUND_SUPPORTED_LANGUAGES in fusion includes "sql"
…, re-CWE dangerous-op rules to cwe-284
leenk7991
approved these changes
Aug 24, 2026
Contributor
Author
|
Done — added "sql" to the backend arm in |
Ibrahimrahhal
approved these changes
Aug 25, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: https://linear.app/corgea/issue/COR-1861/add-sql-language-support-to-sighthound
Read this first
SQL support already ships in production.
mainnever had it.The binary Fusion vendors and runs today is
sighthound 0.1.2, built from commit6dfc357.6dfc357is not an ancestor ofmain(git merge-base --is-ancestor 6dfc357 main→ non-zero,measured).
mainis0.1.1.rules/sqlnever appeared anywhere inmain's reachable history andwas never deleted from it — this is unported work, not a regression.
So this PR is a port, not new development. It takes the fork's SQL implementation and lands it
on
main, with two deliberate false-positive downgrades and one engine fix that the port exposed.1. The fork divergence, both directions
6dfc357sharesmain's root commit but forked at77e388e(2025-09-04).mainhas taken 135commits since; the fork took seven. Both trees carry rule packs the other lacks
(measured with
git ls-tree --name-only <ref> rules/):6dfc357)rules/sql,rules/xml,rules/properties,rules/configmainrules/csharp,rules/go,rules/java,rules/objectscript,rules/php,rules/rubySIGHTHOUND_SUPPORTED_LANGUAGESatfusion/fusion/config.py:363lists exactly the fork's set —javascript,typescript,tsx,html,xml,sql,properties,config. The Fusion gate isbuilt around the fork, not around
main.2. Do NOT bump
mainto0.1.2fusion/Dockerfile:40hard-fails the image build unless the binary reports exactly0.1.2:Cargo.toml:3stays atversion = "0.1.1", deliberately. That pin is the only thing preventing are-vendor from
mainfrom silently dropping the fork-only languages, so it is left failingloudly. Before this PR, four gated languages were at risk of vanishing on a re-vendor (
xml,sql,properties,config); after it, three.No
fusion/changes are in this PR. The gate already contains"sql"— nothing there needschanging. Merging this PR therefore cannot reach production on its own; that is intended.
3. The blocker: combined mode hard-errored on a search-only rule pack
This is the most important thing to check in this PR.
Fusion invokes the binary once per file, at
fusion/fusion/agents/sighthound_scanner.py:71:No language argument, no
--simple-analysis— default combined mode.run_selected_analysis(
src/main.rs:104-131) runsrun_simple_analysisfirst, then appends the result ofrun_taint_analysis_with_verbosity(…, verbose_mode = false).All 11 SQL rules are
mode: "search".run_taint_analysis_with_verbosity(src/scanner/modes.rs)hard-errored whenever
taint_rules_count == 0. So the first port build returned:on the one invocation that matters, where production
0.1.2returned valid JSON. Two of theticket's acceptance criteria failed ("SQL files are included in the normal scan path" and
"produces valid findings in the expected output format").
The fix
verbose_modealready distinguished the two callers:true= explicit--taint-analysis(
modes.rs:485),false= the combined second pass.It must be
Ok(Vec::new())and not a search-pass re-run.run_selected_analysisappends thisresult to the simple pass, so re-running search there would double-count every finding.
Measured after the fix
Fusion's exact invocation against
tests/test_files/strictness_languages/sql/-style SQL, measuredthis session against the vendored production binary (
fusion/vendor/sighthound/osx/sighthound):2 findings, byte-identical to production, exit 0. And the other half still fails honestly:
Blast radius and side effect
Blast radius is exactly "rule sets with zero taint rules", which previously always hard-errored.
The change can only turn an error into a result; it cannot change any finding.
Side effect (measured): an ObjectScript-only scan now exits 0 instead of erroring.
rules/objectscript/andrules/html/are also search-only, so they tripped the same guard. Sameroot cause, same class of fix.
Regression coverage
Two tests in
tests/strictness/language_coverage.rspin both halves:sql_default_combined_mode_succeeds_without_taint_rules— asserts combined mode producesfindings and that its finding set equals
--simple-analysis's, so a future "fix" thatre-runs the search pass and doubles the findings fails here.
sql_explicit_taint_analysis_still_errors_without_taint_rules— asserts non-zero exit and theNo taint flow rules foundmessage.Validated by reverting the fix: with the
if !verbose_mode { return Ok(Vec::new()); }blockremoved, the first test fails with the original
No taint flow rules founderror.The two no-op lines in the same hunk
The diff also replaces
show_progress && verbose_modewithreportin two places.reportis apre-existing binding on
main(let report = show_progress && verbose_mode;, already passed toScanContext::new), so these are a pure rename with zero behavior change. They exist becausereusing the binding brought
run_taint_analysis_with_verbosityback to CCN 13(measured:
run_taint_analysis_with_verbosity@524-605@src/scanner/modes.rs, CCN 13).4. The two deliberate rule divergences from production (D2)
rules/sql/sql_security.rondiffers fromgit show 6dfc357:rules/sql/sql_security.ronbyexactly three hunks — verified with
git diff --no-index <(git show 6dfc357:rules/sql/sql_security.ron) rules/sql/sql_security.ron.Everything else in the pack is a byte-verbatim port.
These two are the only intended findings-behavior differences between this PR and the
0.1.2binary. Both are the same shape: keep the detection, stop it overstating severity.
D2a —
sql-injection-001fired Critical on a bare"||"Production reports
SELECT first_name || ' ' || last_name FROM employees;— ordinary columnconcatenation — as a Critical SQL Injection, at Medium confidence.
patterns: Some([ "CONCAT(*", - "||", "SELECT * WHERE", "SELECT * FROM * WHERE", ]),"||"moves to a new rule at Low severity / Low confidence:( id: Some("sql-injection-002"), name: Some("String Concatenation Operator"), category: Some("sql-injection"), mode: "search", patterns: Some([ "||", ]), finding_type: Some("SQL Injection"), severity: Some("Low"), confidence: Some("Low"), cwe_id: Some("cwe-89"), description: Some("The || operator may be building dynamic SQL, but substring matching cannot distinguish that from ordinary column concatenation"), file_types: Some((extensions: Some([".sql"]))), tags: Some(["sql-injection", "database", "cwe-89"]) ),sql-injection-001keeps its three glob patterns at Critical / Medium. The finding is downgraded,not deleted.
Rejected: deleting
"||"outright — the remaining patterns are globs anchored at byte 0, whichwould leave the rule nearly inert. Also rejected: narrowing to
"' ||"— verified not to work, thebenign case contains that substring too.
D2b —
sql-comment-001reported ordinary comments as SQL InjectionMeasured against the production binary this session — a file containing
-- inline comment test/CREATE TABLE t (id INT); -- trailing comment//* block */:An ordinary trailing
--comment reported as SQL Injection. Nearly every real.sqlfile carries acomment, so this fires at far higher volume than D2a — mislabelled as injection. (The line moves
from 2 to 1 because of Δ1 in §6, not because of this change.)
Patterns (
["--", "/*", "#"]) unchanged;confidencewas alreadyLowin the fork.Note on the
unlessclause (-- Note:,-- TODO, …): it is dead code in both trees.main'sUnifiedRule(src/models.rs:153-236) has nounlessfield and serde ignores it, and6dfc357:src/never references one. It disarms nothing. It is kept as ported —rules/javascript/frontend_security.ron:20carries the same inert field, so removing it here wouldbe an undeclared divergence.
Rejected: deleting the rule outright — customers filtering on it would see findings vanish rather
than drop.
5. Honest limitations, now documented in
README.mdThe ticket asks for documented dialect limitations. The new SQL row (
README.md:42) and thelimitations section under the language table state them plainly, because they are severe:
whole file, not against parsed statements. The tree-sitter JavaScript grammar is wired in as a
no-op host so
call_node_types()names the parse-tree root, yielding one node per file whose"function name" is the entire file text.
*match as substrings anywhere in the file. Patternscontaining
*are globs evaluated against the whole file, so they are anchored at byte 0 —EXEC(@sql);on line 1 is reported; the identical statement on line 2 is not.end_lineis always EOF. Start lines are accurate.three: dynamic SQL inside a stored-procedure body,
CREATE USER … IDENTIFIED BY '…', andGRANT ALL … TO PUBLICare not detected..sql/.ddl/.dml: all three are detected as SQL (src/scanner/utils.rs:266) anddiscovered in explicit mode (
src/scanner/vulnerability_scanner.rs:109), but only.sqlproduces findings. Every rule in the pack carries
file_types: Some((extensions: Some([".sql"]))), andrule_applies_to_file(
src/scanner/utils.rs:145-192) rejects the others. Measured under the production binary:identical
EXEC(@sql);ina.ddl,b.sql,c.dml→ onlyb.sqlfired. The gate arms are keptas a faithful port (the fork has the
should_include_filearm at6dfc357:src/scanner/core.rs:1324),the limitation is documented, and
sql_explicit_mode_scans_ddl_and_dml_but_only_sql_reportspins the zero result so a future reader does not "fix" the gate.
SKIP_DIRSexcludes files beneath it, matched by whole-path-component equality. Measured: theconstant at
src/config.rs:27has 18 entries; the ones a SQL user will hit aretests,test,vendor,build,dist,target,node_modules.--include-test-fixturesreopenstests/and
test/only.Expect both false positives (a benign
||is reported, now atLow) and false negatives (dynamicSQL that is not the first thing in the file is missed).
6. Measured engine-drift delta vs the production
0.1.2binary (D3)main's engine is 135 commits ahead of the fork (scanning_logic.rs/modes.rssplit, ASTprovenance), so ported rules can produce different findings than production. Same 17-file probe
corpus,
--output-format json, both columns. Recorded for review, not chased — per the lockeddecision, a delta is information, not a failed port.
0.1.2main(verbatim port)proc.sqlexecfirst.sqlexeclater.sqlconcat.sqltautology.sqlpipe.sqlclean.sqlcmt.sqlcmt2.sqldrop.sqltrunc.sqlunion.sqlsleep.sqlimmediate.sqlloc.sqlprobe.ddlprobe.dml17 of 17 fixtures reproduce production. The two deltas:
--comment firessql-comment-001undermain's engine; it did not under thefork's. Production returns 0 for
cmt.sql(-- a plain comment+SELECT id FROM t;);mainreturns one finding.
unlessis inert in both trees, so it is not the cause. This makes D2b morevaluable than its own rationale assumed:
mainmislabels more ordinary SQL than production did.mainhonors the rule's declaredconfidence; the fork's engine did not.sql-comment-001declaresconfidence: Some("Low"). Production reportsMedium;mainreportsLow.mainis the more faithful reader of the rule data.After D2 is applied on top, three lines move and the total stays at 18 — no finding is lost:
pipe.sqlCritical/Medium → Low/Low (D2a);cmt.sqlandcmt2.sqlSQL Injection/Medium → CodeSmell/Low (D2b).
concat.sqlis unchanged, which proves removing"||"did not disturbCONCAT(*.Existing behavior is unchanged
Full
tests/test_filescorpus, non-.sqlfindings only: 471 before, 471 after, empty diff.Re-verified after the D2 edits.
Caveat worth recording: finding order is nondeterministic. Two consecutive scans of the same
unchanged corpus on clean
mainreturn the same 471 findings in a different array order — the scanpath is
rayon-parallel, andjq -Ssorts keys, not array elements. Any before/after comparisonmust sort (
sort_by(.file, .line, .finding_type, .snippet)) to be meaningful. Pre-existing andunrelated to SQL, but it will bite the next person who diffs two runs.
7. Verification
make cimake acceptanceSQL rules catch the unsafe fixturemake complexitybuild_sarif_log@189-305@src/scanner/output.rs, length 117 (threshold> 100). This is the inherited clean-mainbaseline at529cbd7, not caused here, and deliberately not fixed. No second warning appearedmake agents-md-driftmaintoo (diverges at line 43) — inherited. NoCLAUDE.mdorAGENTS.mdedit in this PR. It happens to pass inside this worktree only because the git-ignoredCLAUDE.mdmirror is absent here; that is a worktree artifact, not a fixmake checkmaintoo —cmd_check(harness.rs:1023-1058) includes the drift gate in its results array, somake checkcannot be green untilmake sync-agents-mdruns.make cinever runs the drift gate, somake ciis the gate held greencargo build --release --no-default-features --features sqlsql = ["tree-sitter-javascript"]buys over the fork'ssql = []. Its warnings match the--features pythonand--features htmlcontrols exactly, so they are the inherited single-feature-build patterntext,json,csv,sarif) verified on the 18-finding probe corpus. The four finding types SQL introduces with no counterpart inmain's corpus —Dangerous Operation,Performance Issue,Authentication Bypass,Code Smell— plusLowseverity andcwe-1240all round-trip. CSV parsed with a real CSV reader (snippets contain newlines, sowc -lovercounts)Two things worth flagging so a reviewer is not surprised — they are different sets of four:
make cireportsCRAP: 4 function(s) exceed 30(advisory, non-blocking):sink_patterns_for_finding@671-720@src/scanner/scanning_logic.rs(70.4),is_js_bundler_config@40-47@src/scanner/vulnerability_scanner.rs(42.0),extract_parameter_name@1602-1630@src/scanner/scanning_logic.rs(42.0),fmt@380-405@src/scanner/prefilter.rs(33.7). None is touched by this PR.make ciruns clippy without--all-targets. A separatecargo clippy --all-targets -- -D warningsrun shows 4 pre-existing lints, all in files this PR does not touch (measured):
tests/unit/django_xss_prevention_tests.rs:113(length comparison to one),tests/unit/django_xss_prevention_tests.rs:128(unneededreturn),harness.rs:1515(unnecessaryclone),src/scanner/output.rs:318(items after a test module).SARIF note, so a reviewer does not read it as a bug:
rule_id_for(src/scanner/output.rs:193)keys SARIF rule objects on the lowercased
cwe_id, and ten of the eleven SQL rules arecwe-89(only
sql-wildcard-001iscwe-1240). SARIF therefore emits two rule objects, not eleven,whose
properties.security-severityis the group maximum. Inherited production behavior. Severityassertions in the new tests are made at the result level accordingly — a test asserting "a new
rule object appears for
sql-injection-002" would fail on a correct implementation.8. Zero complexity headroom — flag for future work
load_embedded_rules(src/rules.rs) is now at CCN 15 against amake complexityceiling of15 (measured:
uvx lizard@1.22.2 -l rust src/rules.rs→load_embedded_rules@136-175@src/rules.rs,CCN 15). It was 14 before; the new
"sql" =>match arm's?costs the +1. It reads at exactly theceiling, which is why it produces no warning — the gate fires on
> 15, so the separatelizardrun is the only way to see it.
Language #12 cannot be added without refactoring
load_embedded_rules's match arms into a tablefirst. That is what blocks porting the fork's other three packs.
9. Open question for the requester
The requester (Ibrahim, via Slack) asked for SQL support that already ships in production. We do
not know which trigger applies, and the two readings need different fixes:
mainor the OSS repo and saw norules/sql. This PR is the complete fix.production already runs the SQL-capable binary, and
mainat0.1.1cannot get past Fusion'sversion pin. That would need a separate investigation into Fusion routing or rule narrowness.
Please confirm which reading applies.
10. Follow-ups, explicitly not in this PR
--code-type backendsilently scans zero SQL files.src/code_type_detector.rs:250-252hasno
"sql"arm, so.sqlfalls through toCodeType::Unknownwhile every other backend languageis matched. Real defect; needs a ticket. Not a production issue today — Fusion does not pass
--code-type; its command is exactly["sighthound", "--output-format", "json", path](
fusion/fusion/agents/sighthound_scanner.py:71).xml,properties,config) — blocked on theload_embedded_rulesrefactor in §8.src/models.rs:297's positional[LANGUAGE]help stringchanged, so per
AGENTS.md:46the skill atCorgea/skills→plugins/sighthound/skills/sighthound/SKILL.mdneedssqladded. It enumerates languages inthree places (frontmatter
description, theLanguage values:list, and## Limitations).Different repo, out of scope here.
src/config.rs:21ESTIMATED_LANGUAGES: usize = 6is stale — the registry now has 12languages. Pre-existing, deliberately untouched.
Scope fence
No tree-sitter SQL grammar. No dialect choice. No new or improved rules beyond D2a/D2b. No
prefilter change. No new control surface. No version bump. No re-vendoring. No
fusion/edits. Nobuild_sarif_logfix. NoESTIMATED_LANGUAGESfix. NoAGENTS.mdorCLAUDE.mdedit.