SecurityTxt (SecurityTxt v1.0.0)

View Source

Parse, validate, and serialize RFC 9116 security.txt files in Elixir.

The package has no runtime dependencies. Parsing returns structured diagnostics, convenience accessors for registered fields, and OpenPGP cleartext detection. Serialization builds canonical unsigned documents from validated keyword options.

Quick start

expires =
  DateTime.utc_now()
  |> DateTime.add(2 * 365 * 24 * 60 * 60, :second)
  |> DateTime.to_iso8601()

result =
  SecurityTxt.parse("""
  Contact: https://example.com/report
  Expires: #{expires}
  Policy: https://example.com/security-policy
  Preferred-Languages: en, tr
  """)

result.valid
#=> true

result.contact
#=> ["https://example.com/report"]

Enum.map(result.recommendations, & &1.code)
#=> ["long_expiry", "not_signed"]

Serialize

expires =
  DateTime.utc_now()
  |> DateTime.add(2 * 365 * 24 * 60 * 60, :second)
  |> DateTime.to_iso8601()

SecurityTxt.serialize(
  comments: ["Security contact for example.com"],
  contact: ["mailto:security@example.com", "https://example.com/report"],
  expires: expires,
  canonical: "https://example.com/.well-known/security.txt",
  csaf: "https://example.com/.well-known/csaf/provider-metadata.json",
  encryption: "openpgp4fpr:0123456789ABCDEF",
  policy: "https://example.com/security-policy",
  preferred_languages: ["en", "tr"]
)

See the README for the full API, diagnostic codes, and scope.

Summary

Types

A diagnostic produced while processing a security.txt file.

A parsed RFC 9116 field.

The public result returned by the parser.

Functions

Parses and validates a complete security.txt string.

Builds a validated unsigned security.txt document.

Types

diagnostic()

@type diagnostic() :: %{
  code: String.t(),
  message: String.t(),
  line: pos_integer() | nil
}

A diagnostic produced while processing a security.txt file.

field()

@type field() :: %{name: String.t(), value: String.t(), line: pos_integer()}

A parsed RFC 9116 field.

result()

@type result() :: %{
  valid: boolean(),
  errors: [diagnostic()],
  recommendations: [diagnostic()],
  notifications: [diagnostic()],
  fields: [field()],
  signed: boolean(),
  contact: [String.t()],
  expires: String.t() | nil,
  acknowledgments: [String.t()],
  canonical: [String.t()],
  csaf: [String.t()],
  encryption: [String.t()],
  hiring: [String.t()],
  policy: [String.t()],
  preferred_languages: [String.t()]
}

The public result returned by the parser.

Functions

parse(content)

@spec parse(String.t()) :: result()

Parses and validates a complete security.txt string.

Parsing malformed input does not raise. Input over 32,768 UTF-8 bytes is rejected before fields are parsed. The result includes source-order fields, convenience accessors, and diagnostics separated by severity.

Examples

expires =
  DateTime.utc_now()
  |> DateTime.add(2 * 365 * 24 * 60 * 60, :second)
  |> DateTime.to_iso8601()

SecurityTxt.parse("""
Contact: https://example.com/report
Expires: #{expires}
Policy: https://example.com/security-policy
Preferred-Languages: en, tr
""")

serialize(options)

@spec serialize(keyword()) :: String.t()

Builds a validated unsigned security.txt document.

:contact and :expires are required. URI and language fields accept a string or non-empty string list; :comments accepts a non-empty string list. An :expires value may be a current RFC 3339 string or a DateTime.

The output uses LF line endings, canonical field order, and one trailing newline. Invalid options raise ArgumentError.

Examples

expires =
  DateTime.utc_now()
  |> DateTime.add(2 * 365 * 24 * 60 * 60, :second)
  |> DateTime.to_iso8601()

SecurityTxt.serialize(
  comments: ["Security contact for example.com"],
  contact: ["mailto:security@example.com", "https://example.com/report"],
  expires: expires,
  canonical: "https://example.com/.well-known/security.txt",
  csaf: "https://example.com/.well-known/csaf/provider-metadata.json",
  encryption: "openpgp4fpr:0123456789ABCDEF",
  policy: "https://example.com/security-policy",
  preferred_languages: ["en", "tr"]
)