Rules reference
Every rule the RuleFix engine runs — for agents over MCP and for repository scans — grouped by the framework its findings are filed under, with a before/after fix for each. Website checks are listed in Website scans.
On this page
All rules at a glance
18 rules: 9 high, 8 medium, 1 low severity by default. A rule's id is framework.rule-name, where the framework is the primary one its findings are filed under; it can map to several frameworks and runs whenever any of them is selected.
| Rule | Severity | Frameworks |
|---|---|---|
| Aadhaar number stored in plaintextdpdp.aadhaar-plaintext | high | DPDP |
| Protected health information written to logshipaa.phi-in-logs | high | HIPAA, GDPR |
| Deprecated cipher or insecure modeiso-27001.deprecated-cipher | high | ISO 27001, PCI DSS, SOC 2 |
| Card number (PAN) stored or hardcodedpci-dss.card-data-storage | high | PCI DSS |
| Card security code (CVV/CVC) storedpci-dss.cvv-storage | high | PCI DSS |
| Hardcoded secret or credentialsoc-2.hardcoded-secret | high | SOC 2, ISO 27001 |
| JWT accepted without signature verificationsoc-2.jwt-insecure | high | SOC 2, ISO 27001 |
| SQL built from untrusted inputsoc-2.sql-injection | high | SOC 2, ISO 27001, PCI DSS |
| TLS certificate verification disabledsoc-2.tls-verification-disabled | high | SOC 2, ISO 27001, PCI DSS |
| Personal data written to logsdpdp.pii-in-logs | medium | DPDP, GDPR, LGPD |
| User record created without consent evidencegdpr.consent-capture | medium | GDPR, DPDP, LGPD |
| Personal data sent to analytics or tracking toolsgdpr.pii-to-analytics | medium | GDPR, DPDP, CCPA, LGPD |
| Sensitive personal data column without encryptiongdpr.unencrypted-pii-column | medium | GDPR, DPDP, HIPAA, PCI DSS, LGPD |
| Predictable random values used for secretsiso-27001.insecure-random | medium | ISO 27001, SOC 2 |
| MD5/SHA-1 used for passwords or tokensiso-27001.weak-hash | medium | ISO 27001, SOC 2, PCI DSS |
| CORS allows any origin with credentialssoc-2.cors-wildcard | medium | SOC 2 |
| Unencrypted HTTP connectionsoc-2.insecure-transport | medium | SOC 2, ISO 27001, PCI DSS |
| Tracking without a Global Privacy Control / opt-out checkccpa.gpc-not-honored | low | CCPA |
- Severity
- high
- Frameworks
- DPDP
- Severity
- high
- Frameworks
- HIPAA, GDPR
- Severity
- high
- Frameworks
- ISO 27001, PCI DSS, SOC 2
- Severity
- high
- Frameworks
- PCI DSS
- Severity
- high
- Frameworks
- PCI DSS
- Severity
- high
- Frameworks
- SOC 2, ISO 27001
- Severity
- high
- Frameworks
- SOC 2, ISO 27001
- Severity
- high
- Frameworks
- SOC 2, ISO 27001, PCI DSS
- Severity
- high
- Frameworks
- SOC 2, ISO 27001, PCI DSS
- Severity
- medium
- Frameworks
- DPDP, GDPR, LGPD
- Severity
- medium
- Frameworks
- GDPR, DPDP, LGPD
- Severity
- medium
- Frameworks
- GDPR, DPDP, CCPA, LGPD
- Severity
- medium
- Frameworks
- GDPR, DPDP, HIPAA, PCI DSS, LGPD
- Severity
- medium
- Frameworks
- ISO 27001, SOC 2
- Severity
- medium
- Frameworks
- ISO 27001, SOC 2, PCI DSS
- Severity
- medium
- Frameworks
- SOC 2
- Severity
- medium
- Frameworks
- SOC 2, ISO 27001, PCI DSS
- Severity
- low
- Frameworks
- CCPA
- Severity can vary per finding. Some rules raise or lower it for a specific occurrence — for example a password hashed with MD5 is high, a token hashed with it is medium.
- Tests and fixtures (
__tests__/,*.test.ts,fixtures/,test_*.py…) usually hold fake data, so each rule either reports as usual, lowers findings to low, or skips test files — shown on every rule below. - Most rules ignore matches inside comments, and code is only ever read as text — it is never executed or imported.
DPDP rules
Rules filed under Digital Personal Data Protection Act, 2023 (2). These rules filed under another framework also apply to DPDP: gdpr.consent-capture, gdpr.pii-to-analytics, gdpr.unencrypted-pii-column.
Aadhaar number stored in plaintext
dpdp.aadhaar-plaintext
Languages: Any text file · Test files: reported as usual
- What it checks
- Finds Aadhaar numbers hardcoded in code, config or seed data (12 digits, Verhoeff-checksum validated) and Aadhaar fields written to a database, browser storage or cookie without masking, tokenisation or encryption.
- Why it matters
- Aadhaar is a lifelong national identifier linked to biometrics, bank accounts and government services; once exposed it cannot be reissued like a password. UIDAI requires entities that store Aadhaar numbers to keep them encrypted in a dedicated Aadhaar Data Vault and to display only the last four digits. Under India's DPDP Act, 2023 a data fiduciary must take reasonable security safeguards to prevent a personal data breach, and a plaintext Aadhaar column or literal is one of the first things an auditor or attacker will look for.
- How to fix
- Do not persist the full Aadhaar number unless a law or regulation requires it; prefer the UIDAI reference key / virtual ID or store only the last 4 digits.
- If you must store it, encrypt it with a KMS/HSM-managed key in an isolated vault table and keep only an opaque reference token in application tables.
- Mask on display (
XXXX XXXX 1234). - Remove real Aadhaar numbers from code, fixtures and seed data and replace them with obviously synthetic values.
- Restrict and audit access to the vault.
await prisma.kyc.create({ data: { userId, aadhaar: input.aadhaar } });References: UIDAI — Aadhaar Data Vault circular
Personal data written to logs
dpdp.pii-in-logs
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: skipped
- What it checks
- Flags logging statements whose arguments include personal-data fields such as email, phone/mobile, Aadhaar, PAN, date of birth, postal or IP address, SSN, passport number, full name or password. High severity when the field is a password, Aadhaar, PAN or SSN.
- Why it matters
- Logs are copied to log aggregators, error trackers, support tooling and backups, are read by many people, and are rarely covered by deletion or access requests. Personal data that lands there escapes the purpose it was collected for. India's DPDP Act, 2023 requires data fiduciaries to apply reasonable security safeguards and to erase data once the purpose is served; GDPR Art. 5(1)(c) (data minimisation) and Art. 32 (security of processing) set the same expectation. Passwords and government identifiers in logs are directly usable for account takeover or identity fraud.
- How to fix
- Log a stable, non-identifying reference instead of the value (internal user id, request id).
- If the value is genuinely needed for debugging, mask it (
a***@example.com, last 4 digits only) or hash it with a keyed HMAC. - Never log passwords, OTPs, Aadhaar, PAN or SSN in any form.
- Configure a redaction list in the logger (pino
redact, winston format, structlog processor, Logback masking) so new call sites are covered. - Purge existing log data that contains the field and set a retention period.
logger.info(`Password reset requested for ${user.email}`);References: OWASP Logging Cheat Sheet — data to exclude · Digital Personal Data Protection Act, 2023
GDPR rules
Rules filed under General Data Protection Regulation (3). These rules filed under another framework also apply to GDPR: hipaa.phi-in-logs, dpdp.pii-in-logs.
User record created without consent evidence
gdpr.consent-capture
Languages: JavaScript, TypeScript, Python, Ruby, PHP, Java, Go, SQL · Test files: skipped
- What it checks
- Flags user/customer/account creation (Prisma, Django, Mongoose, Sequelize, ActiveRecord, Supabase, MongoDB, raw SQL INSERT INTO users) that stores an email address or phone number without any consent-related field — consent flag or timestamp, accepted terms/privacy-policy version, or marketing opt-in — in the same statement.
- Why it matters
- When consent is the lawful basis for processing (typically marketing email/SMS, and many sign-up flows), you must be able to show that the person consented, to what, and when. GDPR Art. 7(1) puts the burden of proof on the controller, and India's DPDP Act, 2023 likewise requires a data fiduciary to be able to prove that notice was given and consent obtained. A sign-up path that writes contact details without recording the consent event leaves no evidence when a regulator or user asks.
- How to fix
- Capture an explicit, unticked-by-default consent choice in the sign-up form for each purpose (e.g. terms/privacy notice, marketing).
- Persist it in the same write as the user:
termsAcceptedAt,privacyPolicyVersion,marketingOptIn(boolean) andmarketingOptInAt, or insert a row into aconsentstable in the same transaction. - Record the notice/policy version shown and the source (web, app, API).
- Provide an equally easy way to withdraw consent and record withdrawals the same way. If consent is not your lawful basis for this data, document the basis instead and suppress this finding with
rulefix-ignore gdpr.consent-capture.
await prisma.user.create({
data: { email: input.email, name: input.name },
});References: EDPB Guidelines 05/2020 on consent
Personal data sent to analytics or tracking tools
gdpr.pii-to-analytics
Languages: JavaScript, TypeScript, Python, Ruby, PHP, config & other text · Test files: skipped
- What it checks
- Flags analytics, product-tracking and session-replay SDK calls (Segment, PostHog, Mixpanel, Amplitude, Heap, FullStory, LogRocket, CleverTap, Braze, gtag/dataLayer, Intercom, Sentry.setUser) that send raw email addresses, phone numbers, names, postal addresses or dates of birth as identities or properties.
- Why it matters
- Sending direct identifiers to a third-party tool discloses personal data to another processor, often in another country, for a purpose (product analytics, advertising) the user may not have agreed to. GDPR Art. 25 (data protection by design and by default) expects you to minimise and pseudonymise by default, and many analytics vendors' terms forbid sending PII at all. Under CCPA/CPRA, sharing identifiers with ad-tech can be a 'sale' or 'share' that requires an opt-out.
- How to fix
- Identify users with your internal, opaque user id (
analytics.identify(user.id)), never the email or phone. - Drop name, email, phone, address and date-of-birth properties from tracked events; send coarse attributes instead (plan, country, signup cohort).
- If an ad platform requires matching, send a salted SHA-256 hash only after the user has consented to advertising.
- Gate analytics initialisation on the user's consent choice.
- Turn on the vendor's PII-scrubbing / IP-anonymisation settings and delete already-ingested PII via the vendor's deletion API.
- Identify users with your internal, opaque user id (
posthog.identify(user.id, { email: user.email, name: user.name });References: Segment: best practices for identifying users
Sensitive personal data column without encryption
gdpr.unencrypted-pii-column
Languages: SQL, config & other text, JavaScript, TypeScript, Python, Ruby, Java · Test files: skipped
- What it checks
- Flags database columns and ORM model fields that hold high-risk identifiers or special-category data — Aadhaar, PAN, SSN, passport number, card number, health condition/diagnosis/prescription, date of birth — declared as plain text (TEXT/VARCHAR/String/CharField) with no sign of field-level encryption.
- Why it matters
- Disk or volume encryption does not protect these values from anyone who can query the database, read a backup or exploit SQL injection. GDPR Art. 32 names encryption as an appropriate security measure, and Art. 25 expects protection by design; health data is a special category under Art. 9. PCI DSS requirement 3 requires stored card numbers to be rendered unreadable, HIPAA treats encryption of ePHI as an addressable safeguard, and UIDAI requires Aadhaar numbers to be kept encrypted in a data vault.
- How to fix
- Decide whether you need the full value at all — often the last 4 digits, a verification flag or a tokenised reference is enough.
- Otherwise encrypt at the application layer before writing (AES-256-GCM with a KMS-managed key, envelope encryption) and store the ciphertext in a
bytea/binary or clearly named_encryptedcolumn; use your framework's support where available (Railsencrypts, Django encrypted fields, a Prisma/TypeORM encryption extension or transformer). - For card numbers, use your payment processor's tokenisation and do not store the PAN.
- Add a blind index (HMAC) column if you need equality lookups.
- Migrate existing plaintext rows and drop the old column.
CREATE TABLE patients (
id uuid PRIMARY KEY,
ssn varchar(11),
diagnosis text
);References: OWASP Cryptographic Storage Cheat Sheet
SOC 2 rules
Rules filed under SOC 2 — Trust Services Criteria (6). These rules filed under another framework also apply to SOC 2: iso-27001.deprecated-cipher, iso-27001.insecure-random, iso-27001.weak-hash.
Hardcoded secret or credential
soc-2.hardcoded-secret
Languages: Any text file · Test files: reported as usual
- What it checks
- Finds credentials written directly into source or config: provider API keys and tokens (AWS, Stripe, GitHub, Slack, Google, Supabase, OpenAI, Anthropic, SendGrid, npm), private-key PEM blocks, service-role JWTs, passwords embedded in connection URLs, and high-entropy literals assigned to variables named like keys, tokens, secrets or passwords.
- Why it matters
- Anything committed to a repository is copied to every clone, fork, CI cache and backup, and stays in git history after it is deleted. A leaked key gives whoever finds it the same access your service has. SOC 2's logical-access criteria and ISO/IEC 27001 Annex A 5.17 (authentication information) expect secrets to be issued, stored and rotated under control — a key in source code is outside that control and must be treated as compromised.
- How to fix
- Revoke and rotate the exposed credential with the provider now — removing it from the file does not un-leak it.
- Replace the literal with a runtime lookup (
process.env.NAME,os.environ["NAME"], a secrets manager such as AWS Secrets Manager, GCP Secret Manager, Vault or Doppler). - Add the variable name to
.env.examplewith an empty or placeholder value and make sure real.env*files are in.gitignore. - If the secret reached a shared branch, purge it from history (git filter-repo / BFG) and enable secret scanning / push protection.
- Fail fast at startup when the variable is missing instead of falling back to a hardcoded default.
const stripe = new Stripe("sk_live_••••••••••••••••••••••••");References: OWASP Secrets Management Cheat Sheet · GitHub: removing sensitive data from a repository
JWT accepted without signature verification
soc-2.jwt-insecure
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: skipped
- What it checks
- Flags JSON Web Tokens that are trusted without a verified signature: allowing the
nonealgorithm, PyJWT withverify_signature: False, RubyJWT.decode(token, nil, false), and jsonwebtoken'sjwt.decode()(which never verifies) feeding authorization decisions in a file that never callsverify. - Why it matters
- A JWT's claims are only trustworthy if its signature is checked against your key with an expected algorithm. Without that, anyone can mint a token claiming to be any user or an admin by editing the base64 payload — a complete authentication bypass. SOC 2's logical-access criteria and ISO/IEC 27001 Annex A 8.5 (secure authentication) depend on authentication tokens being unforgeable.
- How to fix
- Verify every token with the library's verify function and pin the expected algorithm:
jwt.verify(token, key, { algorithms: ["RS256"], audience, issuer }),jwt.decode(token, key, algorithms=["RS256"], audience=...)in PyJWT,JWT.decode(token, key, true, { algorithm: "RS256" })in Ruby. - Never include
nonein the allowed algorithms. - Use
decodeonly for non-security purposes such as reading thekidheader before verification. - Validate
exp,audandiss, and fetch public keys from a trusted JWKS endpoint.
- Verify every token with the library's verify function and pin the expected algorithm:
const claims = jwt.decode(token) as JwtPayload;
if (claims.role === "admin") return next();References: OWASP JSON Web Token Cheat Sheet
SQL built from untrusted input
soc-2.sql-injection
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: skipped
- What it checks
- Flags SQL statements assembled by concatenating, interpolating or formatting request input (and Python
cursor.execute/ Prisma$queryRawUnsafecalls given dynamically built SQL) instead of passing values as bound parameters. - Why it matters
- SQL injection lets an attacker change the meaning of a query: read every row (including personal and card data), bypass login, modify or delete data, and sometimes run commands on the database host. It remains one of the most exploited web vulnerabilities. SOC 2 and ISO/IEC 27001 Annex A 8.28 (secure coding) expect injection-safe coding practices, and PCI DSS requirement 6 requires software to be protected against common attacks such as injection.
- How to fix
- Pass values as parameters:
db.query("SELECT * FROM users WHERE id = $1", [id]),cursor.execute("... WHERE id = %s", (id,)),db.Query("... WHERE id = ?", id), JDBCPreparedStatement, PDO prepared statements. - With Prisma use the tagged template
prisma.$queryRaw\... ${id}\`(parameterised) instead of$queryRawUnsafe`. - For identifiers that cannot be parameters (column or table names, sort direction), map user input through an allow-list.
- Validate and type input at the boundary (e.g. zod/pydantic) as defence in depth, and run the database user with least privilege.
- Pass values as parameters:
const rows = await db.query("SELECT * FROM orders WHERE user_id = " + req.params.userId);References: OWASP SQL Injection Prevention Cheat Sheet
TLS certificate verification disabled
soc-2.tls-verification-disabled
Languages: Any text file · Test files: reported as low severity
- What it checks
- Flags code and config that turn off TLS certificate or hostname verification:
rejectUnauthorized: false,NODE_TLS_REJECT_UNAUTHORIZED=0, Pythonverify=False/ unverified SSL contexts, GoInsecureSkipVerify: true, PHPCURLOPT_SSL_VERIFYPEERfalse, RubyVERIFY_NONE, Java trust-all verifiers,curl -k,wget --no-check-certificateandstrict-ssl=false. - Why it matters
- Without certificate verification, HTTPS still encrypts traffic but no longer proves who is on the other end, so anyone able to intercept the connection can impersonate the server and read or alter everything, including credentials and personal data. The setting is often added to get past a dev certificate error and then ships to production. SOC 2 and ISO/IEC 27001 (Annex A 8.24, use of cryptography) expect transmission protections to be effective, and PCI DSS requirement 4 requires trusted certificates for cardholder data in transit.
- How to fix
- Remove the override so the client uses default verification.
- If the server uses a private CA or self-signed certificate, trust that CA explicitly instead (
ca:option in Node,verify="/path/ca.pem"in requests,RootCAsin Go,NODE_EXTRA_CA_CERTS). - Fix the server certificate (hostname, chain, expiry) if that was the original error.
- Keep any test-only relaxation behind an explicit non-production flag that cannot be enabled in production.
const agent = new https.Agent({ rejectUnauthorized: false });References: OWASP Transport Layer Security Cheat Sheet
CORS allows any origin with credentials
soc-2.cors-wildcard
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP, YAML, config & other text · Test files: skipped
- What it checks
- Flags CORS configurations that let any website make credentialed requests:
origin: trueor"*"withcredentials: true, reflecting the request's Origin header into Access-Control-Allow-Origin, or allow-all-origins settings in Django, Flask-CORS, Go and Next.js header configs while credentials are enabled. - Why it matters
- With credentialed CORS open to every origin, any website a logged-in user visits can call your API with the user's cookies and read the responses — account data, personal data, CSRF tokens. Browsers reject a literal
*with credentials, so such configs usually get 'fixed' by reflecting the Origin header, which is the dangerous form. SOC 2's logical-access criteria expect access to be restricted to authorised parties. - How to fix
- Replace the wildcard with an explicit allow-list of trusted origins loaded from configuration (
origin: ["https://app.example.com"], or a function that checks the list). - Only send
Access-Control-Allow-Credentials: truefor origins on that list, and addVary: Origin. - Never reflect the request Origin without checking it against the list.
- If the API is genuinely public, drop credentials and use token auth in headers instead of cookies.
- Replace the wildcard with an explicit allow-list of trusted origins loaded from configuration (
app.use(cors({ origin: true, credentials: true }));References: MDN: CORS and credentials
Unencrypted HTTP connection
soc-2.insecure-transport
Languages: Any text file · Test files: skipped
- What it checks
- Flags
http://URLs to public hosts in code and configuration — API endpoints, webhooks, OAuth callbacks, asset or script origins — where traffic would travel without TLS. Loopback, private-network, reserved and XML/JSON-schema namespace URLs are ignored. - Why it matters
- Plain HTTP can be read and modified by anyone on the network path: credentials, session cookies, personal data and API responses are exposed, and injected responses can compromise clients. SOC 2's logical-access criteria expect data in transit to be protected, ISO/IEC 27001 Annex A 8.24 covers the use of cryptography, and PCI DSS requirement 4 requires strong cryptography when cardholder data crosses open, public networks.
- How to fix
- Change the URL to
https://and confirm the endpoint serves a valid certificate. - Move environment-specific base URLs to configuration and validate at startup that production values use https.
- For services you control, enable HSTS and redirect HTTP to HTTPS.
- If the URL is an internal service on a trusted network, prefer TLS anyway (or mTLS); otherwise document the exception and suppress with
rulefix-ignore soc-2.insecure-transport.
- Change the URL to
const res = await fetch("http://api.partner-payments.io/v1/charges", { method: "POST", body });References: OWASP Transport Layer Security Cheat Sheet
HIPAA rules
Rules filed under Health Insurance Portability and Accountability Act (1). These rules filed under another framework also apply to HIPAA: gdpr.unencrypted-pii-column.
Protected health information written to logs
hipaa.phi-in-logs
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: skipped
- What it checks
- Flags logging statements whose arguments include health fields: diagnosis, medical record number (MRN), prescriptions/medications, patient records, health or medical conditions and history, insurance member IDs and lab results.
- Why it matters
- Health information is among the most sensitive personal data. Under HIPAA, logs that contain PHI become part of the systems that must meet the Security Rule's technical safeguards (45 CFR 164.312: access control, audit controls, transmission security), and the minimum-necessary standard discourages copying PHI into places that do not need it. GDPR Art. 9 treats health data as a special category. Log pipelines and third-party log vendors are a common path for PHI exposure and may require a Business Associate Agreement.
- How to fix
- Replace the PHI value with an opaque identifier (encounter id, internal patient id) that is meaningless outside your system.
- Remove diagnosis, medication, MRN and insurance values from log messages and structured log fields entirely.
- Add PHI field names to the logger's redaction configuration.
- Confirm your log vendor is covered by a BAA and that log retention and access controls match your PHI policy.
- Purge historical logs that contain PHI.
logger.info(f"Updated chart for {patient.name}: {visit.diagnosis}")References: HHS: Summary of the HIPAA Security Rule
PCI DSS rules
Rules filed under Payment Card Industry Data Security Standard (2). These rules filed under another framework also apply to PCI DSS: iso-27001.deprecated-cipher, soc-2.sql-injection, soc-2.tls-verification-disabled, gdpr.unencrypted-pii-column, iso-27001.weak-hash, soc-2.insecure-transport.
Card number (PAN) stored or hardcoded
pci-dss.card-data-storage
Languages: Any text file · Test files: reported as usual
- What it checks
- Flags primary account numbers written to a database, browser storage, cookies or logs (fields like cardNumber, card_number, ccnum), and Luhn-valid card numbers with a real card-brand prefix hardcoded in source. Well-known processor test numbers are reported as low.
- Why it matters
- Storing a PAN brings every system that touches it into PCI DSS scope. PCI DSS requirement 3 (protect stored account data) requires stored PANs to be minimised and rendered unreadable (strong cryptography, truncation, tokenisation or hashing), and logs and browser storage are storage too. Most applications never need the full number: payment processors return a token you can charge again.
- How to fix
- Collect card details with the processor's hosted fields / Elements / SDK so the PAN never reaches your servers.
- Store only the processor's token or payment-method id plus display data (brand, last 4, expiry).
- If you are a PCI-scoped service that must store PANs, encrypt them with keys in an HSM/KMS, keep them in a segmented store, and log only masked values (first 6 / last 4).
- Remove real card numbers from code and seed data; use the processor's documented test numbers in tests.
await db.insert(payments).values({ userId, cardNumber: body.cardNumber, expiry: body.expiry });References: PCI Security Standards Council — document library
Card security code (CVV/CVC) stored
pci-dss.cvv-storage
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP, SQL, config & other text · Test files: skipped
- What it checks
- Flags card verification codes (CVV, CVC, CVV2, CVC2, CID, security code) written to a database, ORM model, browser storage, cookie or log, and schema columns created to hold them.
- Why it matters
- The card verification code is sensitive authentication data. PCI DSS prohibits storing it after authorisation — even encrypted — because it is what lets a stolen card number be used for card-not-present fraud. A CVV column, a CVV in localStorage or a CVV in a log line is a direct PCI DSS violation and a high-value target.
- How to fix
- Never persist the security code anywhere: no database column, cache, queue payload, log, analytics event or browser storage.
- Send it straight from the browser to the payment processor (hosted fields / Elements / tokenisation SDK) so your servers never see it.
- Drop any existing CVV columns and purge historical data, logs and backups that contain it.
- Add
cvv,cvcandsecurityCodeto logger redaction lists.
localStorage.setItem('checkout', JSON.stringify({ cardNumber, expiry, cvv }));References: PCI Security Standards Council — document library
ISO 27001 rules
Rules filed under ISO/IEC 27001 information security (3). These rules filed under another framework also apply to ISO 27001: soc-2.hardcoded-secret, soc-2.jwt-insecure, soc-2.sql-injection, soc-2.tls-verification-disabled, soc-2.insecure-transport.
Deprecated cipher or insecure mode
iso-27001.deprecated-cipher
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: reported as low severity
- What it checks
- Flags encryption with broken or deprecated primitives: Node's IV-less
crypto.createCipher, DES, 3DES, RC4, RC2, Blowfish, and ECB mode with any cipher (including Java'sCipher.getInstance("AES"), which defaults to ECB). - Why it matters
- DES keys can be brute-forced, 3DES and Blowfish have 64-bit blocks vulnerable to birthday attacks (Sweet32), RC4 has exploitable keystream biases, and ECB encrypts identical blocks to identical ciphertext so structure leaks through.
createCipherderives the key with a single MD5 and reuses the IV, so the same input always encrypts the same way. ISO/IEC 27001 Annex A 8.24 and PCI DSS requirement 3 require strong cryptography for protected data. - How to fix
- Use an authenticated mode of a modern cipher: AES-256-GCM (
createCipheriv("aes-256-gcm", key, iv)with a random 12-byte IV per message, storing iv + auth tag) or ChaCha20-Poly1305; or a high-level library (libsodium secretbox, Google Tink, AWS Encryption SDK). - Derive keys from a KMS or with a KDF (HKDF; scrypt/Argon2 for passwords), never from a raw passphrase.
- Re-encrypt existing data: decrypt with the old scheme once, write with the new one, and version the ciphertext format.
- Use an authenticated mode of a modern cipher: AES-256-GCM (
const cipher = crypto.createCipher("aes192", password);
const out = cipher.update(data, "utf8", "hex") + cipher.final("hex");References: NIST SP 800-131A Rev. 2 — transitioning cryptographic algorithms
Predictable random values used for secrets
iso-27001.insecure-random
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: skipped
- What it checks
- Flags non-cryptographic random number generators (Math.random, Python random, PHP rand/mt_rand/uniqid, java.util.Random, Go math/rand, Ruby rand) used to create tokens, OTPs, passwords, salts, nonces, session ids, API keys, CSRF tokens or reset/verification codes.
- Why it matters
- These generators are designed for speed and statistical spread, not unpredictability: their internal state can often be recovered from a handful of outputs, letting an attacker predict the next password-reset token, OTP or session id. ISO/IEC 27001 Annex A 8.24 (use of cryptography) and A 5.17 (authentication information) expect secrets to be generated with cryptographically secure methods.
- How to fix
- Use the platform CSPRNG: Node
crypto.randomBytes(32).toString("base64url"),crypto.randomUUID()orcrypto.randomInt()for OTP digits; browserscrypto.getRandomValues(); Pythonsecrets.token_urlsafe()/secrets.randbelow(); JavaSecureRandom; Gocrypto/rand; PHPrandom_bytes()/random_int(); RubySecureRandom. Use at least 128 bits of entropy for tokens, and keep OTPs short-lived and rate-limited.
const resetToken = Math.random().toString(36).slice(2);References: OWASP Cryptographic Storage Cheat Sheet — secure random
MD5/SHA-1 used for passwords or tokens
iso-27001.weak-hash
Languages: JavaScript, TypeScript, Python, Go, Java, Ruby, PHP · Test files: skipped
- What it checks
- Flags MD5 or SHA-1 used to hash passwords, tokens, API keys, OTPs or other secrets (Node crypto, CryptoJS, Python hashlib, Java MessageDigest, Go, Ruby Digest, PHP md5()/sha1()). High severity when the input is a password.
- Why it matters
- MD5 and SHA-1 are fast and broken for collision resistance; billions of guesses per second on commodity GPUs make unsalted or fast-hashed passwords recoverable from a leaked database in hours. Password storage needs a deliberately slow, salted algorithm. ISO/IEC 27001 Annex A 8.24 (use of cryptography) and PCI DSS requirement 8 (strong authentication, protecting stored credentials) expect current, strong algorithms.
- How to fix
- Passwords: use a password-hashing function — Argon2id (preferred), scrypt or bcrypt (cost ≥ 12) — via a maintained library (
argon2,bcrypt,passlib,golang.org/x/crypto/argon2, SpringPasswordEncoder). - Re-hash on next successful login and expire old hashes.
- Tokens/API keys stored for lookup: generate them with a CSPRNG and store SHA-256 (or HMAC-SHA-256 with a server key) of the token.
- Integrity checks/signatures: use SHA-256 or better, or HMAC.
- Passwords: use a password-hashing function — Argon2id (preferred), scrypt or bcrypt (cost ≥ 12) — via a maintained library (
const hash = crypto.createHash("md5").update(password).digest("hex");References: OWASP Password Storage Cheat Sheet
CCPA rules
Rules filed under California Consumer Privacy Act (as amended by CPRA) (1). These rules filed under another framework also apply to CCPA: gdpr.pii-to-analytics.
Tracking without a Global Privacy Control / opt-out check
ccpa.gpc-not-honored
Languages: JavaScript, TypeScript, Python, Ruby, PHP, Java, Go, config & other text · Test files: skipped
- What it checks
- Heuristic: flags advertising pixels and third-party data 'sell/share' calls in files that contain no reference to the Global Privacy Control signal (Sec-GPC header, navigator.globalPrivacyControl) or another do-not-sell/opt-out mechanism.
- Why it matters
- Under the CCPA as amended by the CPRA, cross-context behavioural advertising counts as 'sharing' personal information, and California's regulations require businesses to treat an opt-out preference signal such as Global Privacy Control as a valid request to opt out of sale/sharing. Firing ad pixels for a browser that sends GPC is a common enforcement finding. This rule cannot see consent managers configured outside the file, so treat it as a prompt to verify.
- How to fix
- Before loading ad/tracking pixels, check
navigator.globalPrivacyControl === truein the browser (and theSec-GPC: 1request header on the server) and skip or restrict them when set. - Also honour your 'Do Not Sell or Share My Personal Information' setting for signed-in users and persist it.
- Where a vendor offers a restricted mode, enable it for opted-out users (Meta
fbq('dataProcessingOptions', ['LDU'], 1, 1000), Googlerestricted_data_processing). - If a consent-management platform already handles GPC for this page, confirm it and suppress with
rulefix-ignore-file ccpa.gpc-not-honored.
- Before loading ad/tracking pixels, check
fbq('init', PIXEL_ID);
fbq('track', 'PageView');References: Global Privacy Control specification · California Privacy Protection Agency
Suppressing a finding
When you have reviewed a finding and it is a false positive or an accepted risk, suppress it with a comment naming the rule. It works in any comment syntax (//, #, --, /* */, <!-- -->).
| Marker | Scope |
|---|---|
rulefix-ignore <rule-id> | The line with the comment and the next line |
rulefix-ignore-file <rule-id> | The whole file |
rulefix-ignore <rule-id>- Scope
- The line with the comment and the next line
rulefix-ignore-file <rule-id>- Scope
- The whole file
// rulefix-ignore soc-2.insecure-transport -- internal health check on the private network
const res = await fetch("http://10.0.0.12:8080/health");
// Several rules at once, separated by commas or spaces:
// rulefix-ignore dpdp.pii-in-logs, gdpr.pii-to-analyticsNo wildcards
Rule ids must be listed explicitly — there is no “ignore everything” marker — so every suppression is a visible, rule-specific decision your reviewers can see in the diff. Add a short reason after the id. To hide a finding without touching code, mark the issue Ignored in the dashboard instead.
How findings are identified
Every finding has a fingerprint: the SHA-256 of the rule id, the file path and the offending line with whitespace collapsed. Line numbers are left out, so re-indenting the line or moving it elsewhere in the file keeps the same fingerprint, while changing the offending code produces a new one. Dashboard issues, rulefix_verify_fix and rulefix_resolve_issue all match on it.
The code excerpt stored with a finding is that one line, trimmed to 300 characters, with secret values and identifiers redacted before it leaves the engine.
How the score is calculated
Each scan gets a score from 0 to 100; higher is better, and no findings scores 100. The same formula is used for agent scans, repository scans and website scans.
- Findings are grouped into distinct (rule, file) pairs, so ten copies of the same problem in one file count once. A pair weighs as much as its worst finding: high 12, medium 5, low 1.5.
- Within one rule, pairs are sorted heaviest first and the k-th one (from 0) deducts
weight × 0.6^k. So one rule can never deduct more than 2.5× its weight: 30 for a high rule, 12.5 medium, 3.75 low. score = round(100 − total deductions), clamped to 0–100.
Worked example
| Findings | Deduction |
|---|---|
| Hardcoded secret (high) in 2 files | 12 + 12 × 0.6 = 19.2 |
| Personal data in logs (medium), twice in 1 file | 5 (one pair) = 5 |
| Insecure random (low) in 1 file | 1.5 |
| Score | 100 − 25.7 ≈ 74 |
- Deduction
- 12 + 12 × 0.6 = 19.2
- Deduction
- 5 (one pair) = 5
- Deduction
- 1.5
- Deduction
- 100 − 25.7 ≈ 74
Adding a finding or raising its severity never increases the score. Treat it as a trend for one project, not a compliance grade — framework coverage in the dashboard shows what was actually checked.
