Skip to content

Latest commit

 

History

History
222 lines (180 loc) · 12.8 KB

code-rules.md

File metadata and controls

222 lines (180 loc) · 12.8 KB

Code rules

Static analysis runs exactly your organization's enabled code rules — nothing else. They are records in the cloudsec_code_rule Hive, and LimaCharlie's rules are records there too: there is no hidden built-in pack and no separate override layer. What the Hive holds is what a scan runs. The scan policy decides whether static analysis runs on a repository; code rules decide what it looks for.

In the console they are under Cloud Security → Policies → Code rules: search and filter by language, severity, source, tag and status, turn records on and off, and create, edit (as YAML or JSON) or delete them. Reading them needs cloudsec.get; changing them needs cloudsec.set.

The default rules

When the organization subscribes to ext-cloud-security, LimaCharlie installs its default rule set: its own rules, and a licence-filtered subset of GitLab's open-source sast-rules. Each rule is its own record, so each can be switched off or deleted on its own:

  • the record key is the rule id, e.g. lc.python.cwe-295.tls-verify-disabled for a LimaCharlie rule or go_crypto_rule-tlsversion for a third-party one;
  • tags name the source (limacharlie or gitlab-sast-rules) and the language;
  • a third-party rule's metadata carries its licence (license, license_notice, the full license_text) and the pinned upstream source_url.

What the default rules look for

The defaults are mapped to CWE and cover these classes of weakness. They do not list every language for every check: a class can have rules in only some of its languages.

Class What the rules look for Languages
Injection SQL built by string concatenation or formatting, OS command execution with user-controlled input, dynamic evaluation of code, LDAP, XPath, XSLT and expression-language injection, XML external entities (XXE), template injection, header and response splitting, format strings JavaScript and TypeScript, Python, Go, Java, C#, Ruby, Rust
Path and file handling Path traversal, unsafe extraction of archives and decompression bombs, insecure temporary files, overly permissive file modes, file disclosure JavaScript and TypeScript, Python, Go, Java, C#, Ruby
Request forgery and redirects Server-side request forgery (SSRF), open redirects, permissive CORS JavaScript and TypeScript, Python, Go, Java, C#, Ruby
Cross-site scripting Unescaped output written into HTML, disabled auto-escaping, unsafe template and rendering APIs JavaScript and TypeScript, Python, Go, Java, C#
Unsafe deserialization Deserializing untrusted data with formats that can run code JavaScript and TypeScript, Python, Java, C#, Ruby
Cryptography and transport TLS certificate verification turned off, old TLS versions, weak hashes, ciphers, modes and padding, short keys, weak random number generators, hardcoded keys, SSH host-key checks turned off, cleartext protocols (FTP, Telnet, SNMP v1 and v2) All seven; the cleartext-protocol checks are Python only
Credentials and configuration Hardcoded and empty passwords, anonymous LDAP binds, debug endpoints left enabled, services bound to all network interfaces, cookies missing the HttpOnly or Secure flag Java, Python, Go, C#

Java and Python have the most rules. Ruby and Rust have a smaller core set, mostly injection, TLS verification, weak hashes and hardcoded keys. There are also a few checks for unsafe low-level APIs, such as Node.js buffers and Go's unsafe package.

The defaults have no rules for C and C++, PHP, Kotlin, Scala or Swift. Static analysis can read those languages, so write your own rules to cover them.

Many default rules are low-confidence. A match from a low-confidence rule is filed as INFO, so it stays visible but ranks low, and INFO findings never fail a pull-request check. See Default severity adjustments. Static analysis does not read vendor, node_modules, testdata or __tests__ directories, or *.min.js files. See the reference.

After that they are your records. Edit, disable or delete any of them, LimaCharlie's included. Installing is create-only: a record whose key already exists is never overwritten, and nothing puts back a rule you deleted until you ask for it.

Restore defaults (on the Code rules page) asks for that. The button appears only for users who have both cloudsec.set and ext.request.

  • By default it only re-creates default rules that are missing. Existing records, including defaults you edited or disabled, stay as they are.
  • With Also reset existing default rules to the shipped content, default-keyed records that LimaCharlie installed are also rewritten to its rule, enabled state, tags and comment (acl: tags are kept). Your edits to those records are lost. A default-keyed record that you created yourself, for example by deleting a default and saving a record under the same key, cannot be reset: it is reported as failed. Delete it and restore again to get LimaCharlie's version back.

Records whose key is not a default key are never touched either way, and running it again is safe. The same action is available outside the console:

limacharlie extension request --name ext-cloud-security \
  --action restore_default_code_rules --data '{"overwrite": false}'

It answers with the total, created, overwritten, skipped and failed counts, plus a failures list of {key, error} for the records that could not be written. The list holds at most 50 entries; failed is the full count.

!!! warning "Restoring is authorized by ext.request, not cloudsec.set" The extension writes the records with its own credentials. Anyone who may make extension requests on the organization can re-create deleted defaults and, with overwrite, reset edited or disabled ones, and resource ACL tags are not checked against the caller. Grant ext.request accordingly.

Writing a rule

A record is one Semgrep/Opengrep rule file, stored as its JSON equivalent: {"rules": [ ... ]}, exactly what the YAML rules: document parses to. A record may hold up to 100 rules; use one rule per record when you want to turn rules on and off individually.

# requests-timeout.yaml
rules:
  - id: acme.python.requests-without-timeout
    languages: [python]
    severity: MEDIUM
    message: >-
      This HTTP request has no timeout, so a slow or unresponsive server can hang
      the worker indefinitely. Pass timeout=<seconds>.
    metadata:
      cwe: "CWE-400"
      category: security
    patterns:
      - pattern: requests.$METHOD(...)
      - pattern-not: requests.$METHOD(..., timeout=$T, ...)
      - metavariable-regex:
          metavariable: $METHOD
          regex: ^(get|post|put|patch|delete|head|request)$
limacharlie hive set --hive-name cloudsec_code_rule \
    --key acme.python.requests-without-timeout \
    --input-file requests-timeout.yaml --enabled --tag-add acme

The YAML file is taken as-is and stored as JSON. Pass --enabled: a new record is disabled unless you say otherwise, and a disabled record does not run. The rest is the standard Hive surface — limacharlie hive list, get, enable, disable, delete and validate with --hive-name cloudsec_code_rule, or the /v1/hive/cloudsec_code_rule/<oid>/<key> REST routes. Like the other cloudsec_* Hives, it is not walked by limacharlie sync.

A record is checked when you save it, and a rule that cannot run is rejected with an error naming it:

Field Requirement
id Required. Letters, digits, ., _ and -, at most 256 characters, unique within the record. It is the finding's rule id.
message Required, non-empty.
severity Required: CRITICAL, HIGH, MEDIUM, LOW, or the older ERROR, WARNING, INFO, in any case. ERROR files a HIGH finding, WARNING a MEDIUM one, INFO an INFO one. EXPERIMENT and INVENTORY are refused because they never report.
languages Required, non-empty. Each a language name the engine accepts, including its aliases (js, py, golang, tf, …) and generic / regex for language-independent rules.
matcher Exactly one of pattern, patterns, pattern-either, pattern-regex or match; or mode: taint with non-empty pattern-sources and pattern-sinks; or a taint: block with sources and sinks. Mixing them is refused, because the engine would silently ignore one side. mode is search (the default) or taint.
metadata, options, paths, fix Optional. metadata and options are objects, paths takes only include / exclude lists, and fix is a string. Every other key the engine understands is stored and passed through untouched.

A record is at most 256 KB of JSON; split larger rule files across records.

Saving checks structure, not whether a pattern parses for its language — only the engine can tell that. A rule that fails to load at scan time is reported by name and skipped, and every other rule still runs (see When rules cannot run). A rule id that an earlier record (by key order) already uses is skipped the same way, so keep ids unique across the organization.

Changes apply to scans that start about a minute after the save. A rule change does not rescan anything by itself; rescan the repository (Rescan now, or limacharlie cloudsec code rescan <owner>/<name>) to see its effect immediately.

Limits on the rule set

Limit Value
Rules per record 100
Record size 256 KB of JSON
Enabled rules per organization 5,000
Enabled rule JSON per organization 20 MB

Above either organization limit the whole set is refused, never an arbitrary subset, and static analysis does not run until you disable rules.

When rules cannot run

A problem with the rule set affects static analysis only. The repository reads scan_status: partial and carries the reason in scan_limits; its existing code_weakness findings are kept rather than closed, and its dependency, secret, infrastructure and license findings are complete and still close normally.

Reason Meaning What to do
sast_no_rules No code rule is enabled, so static analysis ran no rules. Enable rules, or Restore defaults.
sast_rules_over_cap:rules / sast_rules_over_cap:bytes More than 5,000 rules, or more than 20 MB of rules, are enabled. Static analysis did not run. Disable rules.
sast_rule_errors Static analysis ran, but some rules failed to load and were skipped. Fix or disable the named rules.
sast_rules_unavailable (optionally :<step>) The rule set could not be prepared for this scan. Nothing; the next scan retries.
sast_rules_hive_unavailable This deployment cannot read code rules yet. Nothing; it is fixed on LimaCharlie's side.

The rules that failed are listed by record key, rule id and the engine's error. On the Code security page, a repository's Details drawer lists them, each linking to its record in Code rules. In the API and limacharlie cloudsec code repos output, a repository that static analysis covered also carries:

Field Meaning
sast_rules_sha256 The digest of the rule set the scan ran.
sast_rules_count How many rules that set held.
sast_rule_errors The rules that failed to load: {record_key, rule_id, message}. rule_id is empty when the record itself is not a rule file.
sast_rule_errors_total The exact number of failed rules. The list may be shorter.

Pull-request checks

A pull-request check uses one rule set for both commits, taken when the check starts, so a rule added in the meantime cannot make existing code look new.

  • No enabled code rules, or the same rules failing to load on both commits. The check's conclusion is unchanged. Its summary adds one line saying what static analysis did not examine, such as "Static analysis ran no rules: this organization has no enabled code rules." or "2 code rule(s) failed to load and were not run on either commit …".
  • Rules that could not be used at all (over the organization limits, not available for the scan, or every enabled record unusable), or different rules failing on the two commits. The check is incomplete: code weaknesses are left out of the comparison, so none can fail the check, and a check with no other new finding concludes neutral, never success. New findings of other classes still count and can still fail gating.fail_on.

See What the check says.

Local scans

Local scans never apply your organization's code rules. See Scan locally or in CI for which rules they run.