Skip to content

feat: add security.txt detection flag#2472

Open
iacker wants to merge 2 commits intoprojectdiscovery:devfrom
iacker:feat/security-txt-detection
Open

feat: add security.txt detection flag#2472
iacker wants to merge 2 commits intoprojectdiscovery:devfrom
iacker:feat/security-txt-detection

Conversation

@iacker
Copy link
Copy Markdown

@iacker iacker commented Apr 9, 2026

Summary

  • add a native --security-txt flag to probe /.well-known/security.txt and /security.txt
  • require HTTP 200 plus a Contact: field, and reject non-plain-text responses to reduce soft-404 noise
  • expose a security_txt boolean in structured output and add focused tests for the helper logic

Testing

  • go test ./runner -run 'Test(NormalizeRequestURIs|AppendCommaSeparatedValue|IsSecurityTxt|Runner_CSVRow)' -count=1
  • go test ./...

Closes #2468

Summary by CodeRabbit

  • New Features

    • Added a CLI flag to detect security.txt files at standard paths (/.well-known/security.txt and /security.txt).
    • Detection results are included in output (JSON output enabled) and a new result field indicates security.txt presence.
  • Tests

    • Added unit tests covering security.txt detection and request-URI preprocessing/validation.

@auto-assign auto-assign bot requested a review from dwisiswant0 April 9, 2026 19:37
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2962898-9e22-4277-bd69-2fb7b87b640e

📥 Commits

Reviewing files that changed from the base of the PR and between a233603 and c00245c.

📒 Files selected for processing (2)
  • runner/runner.go
  • runner/runner_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • runner/runner_test.go
  • runner/runner.go

Walkthrough

Added a new --security-txt CLI flag and Options.SecurityTxt to enable automatic detection of security.txt on standard paths; runner initialization augments match/status/URIs and forces JSON output when enabled; per-response detection sets Result.SecurityTxt using content-type, status, and presence of Contact:.

Changes

Cohort / File(s) Summary
CLI Options & Types
runner/options.go, runner/types.go
Added SecurityTxt bool to Options and registered --security-txt flag; added SecurityTxt bool to exported Result with JSON/CSV/MD/mapstructure tags.
Runner Initialization & Detection Logic
runner/runner.go
When enabled, New() appends "Contact:" to match strings, "200" to match status, adds /.well-known/security.txt,/security.txt to request URIs, forces JSONOutput=true, normalizes RequestURIs into deduplicated slice, and sets Result.SecurityTxt per-response via isSecurityTxt(). Added helpers: appendCommaSeparatedValue, normalizeRequestURIs, isSecurityTxt.
Tests
runner/runner_test.go
Added unit tests for normalizeRequestURIs, appendCommaSeparatedValue, and isSecurityTxt covering status, content-type, presence of Contact:, and deduplication/whitespace cases.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Runner
  participant HTTP as "Target HTTP"
  participant Analyzer

  CLI->>Runner: start with --security-txt
  Runner->>Runner: extend match strings/status/URIs, force JSON, normalize URIs
  Runner->>HTTP: request /.well-known/security.txt (and /security.txt)
  HTTP-->>Runner: response (status, headers, body)
  Runner->>Analyzer: analyze response
  Analyzer-->>Runner: isSecurityTxt? (200 && content-type ok && contains "Contact:")
  Runner-->>Runner: set Result.SecurityTxt and include in JSON output
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniffed the paths both near and far,
Found "Contact:" where the disclosure stars,
A flag, a hop, a tidy trail,
Security.txt — caught without fail! ✨🔎

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add security.txt detection flag' accurately summarizes the main change: adding a new --security-txt flag for detecting security.txt files.
Linked Issues check ✅ Passed The PR comprehensively implements all coding requirements from issue #2468: dedicated --security-txt flag, checks standard paths (/.well-known/security.txt and /security.txt), validates HTTP 200, verifies Contact field presence, rejects non-plain-text responses, and exposes security_txt in output.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the security.txt detection feature: adding options flag, detection logic, request URI preprocessing, helper functions, tests, and output field. No unrelated modifications found.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@neo-by-projectdiscovery-dev
Copy link
Copy Markdown

neo-by-projectdiscovery-dev bot commented Apr 9, 2026

Neo - PR Security Review

No security issues found

Hardening Notes
  • The documentation comments accurately describe the behavior of their associated functions

Comment @pdneo help for available commands. · Open in Neo

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@runner/runner.go`:
- Around line 333-341: The code appends "200" to options.OutputMatchStatusCode
when options.SecurityTxt is true, but parsing into the internal matchStatusCode
slice happens earlier in ValidateOptions()/ParseOptions(), so the appended 200
is ignored; to fix, ensure the status-code parsing runs after you modify
OutputMatchStatusCode — either move the append logic for
options.OutputMatchStatusCode (and any related options.RequestURIs changes) to
before ValidateOptions()/New() in ParseOptions(), or call the same parsing
routine that produces matchStatusCode again after the append so matchStatusCode
includes "200" (refer to options.SecurityTxt, options.OutputMatchStatusCode,
ValidateOptions(), ParseOptions(), New(), and the matchStatusCode parsing
logic).
- Around line 3021-3031: The isSecurityTxt function only checks for the exact
case "Contact:" which can miss valid security.txt field names; update the check
in isSecurityTxt to perform a case-insensitive search for the "contact:" field
(e.g., normalize resp.RawData to lowercase and check for "contact:" or use a
case-insensitive regex) so that variants like "contact:" or "CONTACT:" are
detected while keeping the existing Content-Type and status checks intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 91bc8858-d780-49ff-84dd-f3e20a1b675c

📥 Commits

Reviewing files that changed from the base of the PR and between 1c3f6df and a233603.

📒 Files selected for processing (4)
  • runner/options.go
  • runner/runner.go
  • runner/runner_test.go
  • runner/types.go

Comment on lines +333 to +341
if options.SecurityTxt {
options.OutputMatchString = append(options.OutputMatchString, "Contact:")
options.OutputMatchStatusCode = appendCommaSeparatedValue(options.OutputMatchStatusCode, "200")
options.RequestURIs = appendCommaSeparatedValue(options.RequestURIs, "/.well-known/security.txt,/security.txt")
options.JSONOutput = true
}
if options.RequestURIs != "" {
options.requestURIs = normalizeRequestURIs(options.RequestURIs)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: OutputMatchStatusCode modification occurs after parsing, making status code filter ineffective.

When --security-txt is enabled, this code appends "200" to options.OutputMatchStatusCode. However, ValidateOptions() (called before New() in ParseOptions()) has already parsed OutputMatchStatusCode into the matchStatusCode slice used for filtering at line 1204.

As a result, the 200 status code match won't be enforced when using --security-txt alone, potentially returning non-200 responses.

🐛 Proposed fix: Parse the status code after modification
 if options.SecurityTxt {
     options.OutputMatchString = append(options.OutputMatchString, "Contact:")
     options.OutputMatchStatusCode = appendCommaSeparatedValue(options.OutputMatchStatusCode, "200")
     options.RequestURIs = appendCommaSeparatedValue(options.RequestURIs, "/.well-known/security.txt,/security.txt")
     options.JSONOutput = true
+    // Re-parse matchStatusCode since we modified OutputMatchStatusCode after initial parsing
+    if parsed, err := stringz.StringToSliceInt(options.OutputMatchStatusCode); err == nil {
+        options.matchStatusCode = parsed
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if options.SecurityTxt {
options.OutputMatchString = append(options.OutputMatchString, "Contact:")
options.OutputMatchStatusCode = appendCommaSeparatedValue(options.OutputMatchStatusCode, "200")
options.RequestURIs = appendCommaSeparatedValue(options.RequestURIs, "/.well-known/security.txt,/security.txt")
options.JSONOutput = true
}
if options.RequestURIs != "" {
options.requestURIs = normalizeRequestURIs(options.RequestURIs)
}
if options.SecurityTxt {
options.OutputMatchString = append(options.OutputMatchString, "Contact:")
options.OutputMatchStatusCode = appendCommaSeparatedValue(options.OutputMatchStatusCode, "200")
options.RequestURIs = appendCommaSeparatedValue(options.RequestURIs, "/.well-known/security.txt,/security.txt")
options.JSONOutput = true
// Re-parse matchStatusCode since we modified OutputMatchStatusCode after initial parsing
if parsed, err := stringz.StringToSliceInt(options.OutputMatchStatusCode); err == nil {
options.matchStatusCode = parsed
}
}
if options.RequestURIs != "" {
options.requestURIs = normalizeRequestURIs(options.RequestURIs)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runner/runner.go` around lines 333 - 341, The code appends "200" to
options.OutputMatchStatusCode when options.SecurityTxt is true, but parsing into
the internal matchStatusCode slice happens earlier in
ValidateOptions()/ParseOptions(), so the appended 200 is ignored; to fix, ensure
the status-code parsing runs after you modify OutputMatchStatusCode — either
move the append logic for options.OutputMatchStatusCode (and any related
options.RequestURIs changes) to before ValidateOptions()/New() in
ParseOptions(), or call the same parsing routine that produces matchStatusCode
again after the append so matchStatusCode includes "200" (refer to
options.SecurityTxt, options.OutputMatchStatusCode, ValidateOptions(),
ParseOptions(), New(), and the matchStatusCode parsing logic).

Comment on lines +3021 to +3031
func isSecurityTxt(resp *httpx.Response) bool {
if resp == nil || resp.StatusCode != 200 {
return false
}
contentType := strings.ToLower(resp.GetHeaderPart("Content-Type", ";"))
if contentType != "" && !strings.Contains(contentType, "text/plain") {
return false
}
body := string(resp.RawData)
return strings.Contains(body, "Contact:")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

RFC 9116 security.txt field name case sensitivity

💡 Result:

RFC 9116 specifies that security.txt field names are case-insensitive. It defines the field “name” as a field-name per RFC 5322 and then explicitly states: “Field names are case insensitive (as per Section 2.3 of RFC 5234).” (datatracker.ietf.org)

Citations:


Case-sensitive "Contact:" check may miss valid security.txt files.

Per RFC 9116, field names in security.txt are case-insensitive. The current check strings.Contains(body, "Contact:") will miss valid files using lowercase contact: or mixed case variants.

Suggested fix
 func isSecurityTxt(resp *httpx.Response) bool {
     if resp == nil || resp.StatusCode != 200 {
         return false
     }
     contentType := strings.ToLower(resp.GetHeaderPart("Content-Type", ";"))
     if contentType != "" && !strings.Contains(contentType, "text/plain") {
         return false
     }
-    body := string(resp.RawData)
-    return strings.Contains(body, "Contact:")
+    body := strings.ToLower(string(resp.RawData))
+    return strings.Contains(body, "contact:")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func isSecurityTxt(resp *httpx.Response) bool {
if resp == nil || resp.StatusCode != 200 {
return false
}
contentType := strings.ToLower(resp.GetHeaderPart("Content-Type", ";"))
if contentType != "" && !strings.Contains(contentType, "text/plain") {
return false
}
body := string(resp.RawData)
return strings.Contains(body, "Contact:")
}
func isSecurityTxt(resp *httpx.Response) bool {
if resp == nil || resp.StatusCode != 200 {
return false
}
contentType := strings.ToLower(resp.GetHeaderPart("Content-Type", ";"))
if contentType != "" && !strings.Contains(contentType, "text/plain") {
return false
}
body := strings.ToLower(string(resp.RawData))
return strings.Contains(body, "contact:")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runner/runner.go` around lines 3021 - 3031, The isSecurityTxt function only
checks for the exact case "Contact:" which can miss valid security.txt field
names; update the check in isSecurityTxt to perform a case-insensitive search
for the "contact:" field (e.g., normalize resp.RawData to lowercase and check
for "contact:" or use a case-insensitive regex) so that variants like "contact:"
or "CONTACT:" are detected while keeping the existing Content-Type and status
checks intact.

Addresses CodeRabbit review finding that docstring coverage was 20%.
Adds Go doc comments to all new helper functions and tests:
- appendCommaSeparatedValue
- normalizeRequestURIs
- isSecurityTxt
- TestNormalizeRequestURIs
- TestAppendCommaSeparatedValue
- TestIsSecurityTxt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in flag for security.txt detection (e.g., -security-txt)

1 participant