Skip to content

feat: add security.txt detection flag#2472

Open
iacker wants to merge 1 commit intoprojectdiscovery:devfrom
iacker:feat/security-txt-detection
Open

feat: add security.txt detection flag#2472
iacker wants to merge 1 commit 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 --security-txt CLI flag to detect security.txt files on standard paths (/.well-known/security.txt and /security.txt). Results are reported in JSON output format.
  • Tests

    • Added unit tests for security.txt detection and URI 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

Walkthrough

Added a new --security-txt CLI flag to the httpx runner that automatically detects valid security.txt files on standard paths. When enabled, the feature configures matching rules for HTTP 200 responses containing the required "Contact:" field and forces JSON output.

Changes

Cohort / File(s) Summary
CLI Options & Types
runner/options.go, runner/types.go
Added SecurityTxt boolean field to Options struct with CLI flag registration, and extended Result struct with SecurityTxt field for JSON/CSV/MD serialization.
Runner Initialization & Detection Logic
runner/runner.go
Extended New() initialization to configure security.txt detection (appending "Contact:" match, ensuring 200 status, adding standard URI paths). Added helper functions for URI normalization, comma-separated value concatenation, and response validation via isSecurityTxt().
Test Coverage
runner/runner_test.go
Added unit tests for URI normalization, comma-separated value handling, and security.txt detection across various HTTP response scenarios (status codes, content-types, payload validation).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A flag most grand, --security-txt true,
Now hunters find disclosure files, tried and new!
No more long chains of paths and strings to test,
Just one quick toggle—security.txt quest! 🔐✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: add security.txt detection flag' clearly and concisely describes the main change: adding a new flag for detecting security.txt files.
Linked Issues check ✅ Passed All coding objectives from issue #2468 are met: the PR implements the --security-txt flag, validates HTTP 200 responses, verifies Contact: field presence, and exposes security_txt in structured output.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the security.txt detection feature requested in issue #2468; no out-of-scope modifications were introduced.

✏️ 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

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.

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