Skip to content

Commit

Permalink
Add support for security.txt API
Browse files Browse the repository at this point in the history
  • Loading branch information
Cyb3r-Jak3 committed Dec 20, 2024
1 parent e0e1dcd commit c8e46f8
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/3763.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
security_txt: adds support for security txt apis
```
96 changes: 96 additions & 0 deletions security_txt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package cloudflare

import (
"context"
"errors"
"fmt"
"github.com/goccy/go-json"
"net/http"
"time"
)

type SecurityTXT struct {
Acknowledgements []string `json:"acknowledgments"`
Canonical []string `json:"canonical"`
Contact []string `json:"contact"`
Enabled bool `json:"enabled"`
Encryption []string `json:"encryption"`
Expires *time.Time `json:"expires"`
Hiring []string `json:"hiring"`
Policy []string `json:"policy"`
PreferredLanguages string `json:"preferredLanguages"`
}

type SecurityTXTResponse struct {
Response
Result SecurityTXT `json:"result"`
}

func (api *API) GetSecurityTXT(ctx context.Context, rc *ResourceContainer) (SecurityTXT, error) {
if rc.Identifier == "" {
return SecurityTXT{}, ErrMissingZoneID
}

uri := buildURI(fmt.Sprintf("/zones/%s/security-center/securitytxt", rc.Identifier), nil)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return SecurityTXT{}, err
}

result := SecurityTXTResponse{}
if err := json.Unmarshal(res, &result); err != nil {
return SecurityTXT{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}

if !result.Success {
return SecurityTXT{}, errors.New(result.Messages[0].Message)
}

return result.Result, nil
}

func (api *API) UpdateSecurityTXT(ctx context.Context, rc *ResourceContainer, securityTXT SecurityTXT) (SecurityTXT, error) {
if rc.Identifier == "" {
return SecurityTXT{}, ErrMissingZoneID
}

uri := buildURI(fmt.Sprintf("/zones/%s/security-center/securitytxt", rc.Identifier), nil)
res, err := api.makeRequestContext(ctx, http.MethodPut, uri, securityTXT)
if err != nil {
return SecurityTXT{}, err
}

result := SecurityTXTResponse{}
if err := json.Unmarshal(res, &result); err != nil {
return SecurityTXT{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}

if !result.Success {
return SecurityTXT{}, errors.New(result.Messages[0].Message)
}

return result.Result, nil
}

func (api *API) DeleteSecurityTXT(ctx context.Context, rc *ResourceContainer) error {
if rc.Identifier == "" {
return ErrMissingZoneID
}

uri := buildURI(fmt.Sprintf("/zones/%s/security-center/securitytxt", rc.Identifier), nil)
res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
if err != nil {
return err
}

result := SecurityTXTResponse{}
if err := json.Unmarshal(res, &result); err != nil {
return fmt.Errorf("%s: %w", errUnmarshalError, err)
}

if !result.Success {
return errors.New(result.Messages[0].Message)
}

return nil
}
183 changes: 183 additions & 0 deletions security_txt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package cloudflare

import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"time"
)

func TestSecurityTXT_Get(t *testing.T) {
setup()
defer teardown()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{
"result": {
"acknowledgments": [
"https://example.com/hall-of-fame.html"
],
"canonical": [
"https://www.example.com/.well-known/security.txt"
],
"contact": [
"mailto:[email protected]",
"tel:+1-201-555-0123",
"https://example.com/security-contact.html"
],
"enabled": true,
"encryption": [
"https://example.com/pgp-key.txt",
"dns:5d2d37ab76d47d36._openpgpkey.example.com?type=OPENPGPKEY",
"openpgp4fpr:5f2de5521c63a801ab59ccb603d49de44b29100f"
],
"expires": "2020-12-02T20:24:07.776073Z",
"hiring": [
"https://example.com/jobs.html"
],
"policy": [
"https://example.com/disclosure-policy.html"
],
"preferredLanguages": "en, es, fr"
},
"success": true,
"errors": [],
"messages": []
}`)
}

mux.HandleFunc("/zones/"+testZoneID+"/security-center/securitytxt", handler)

expires, _ := time.Parse(time.RFC3339, "2020-12-02T20:24:07.776073Z")

want := SecurityTXT{
Acknowledgements: []string{"https://example.com/hall-of-fame.html"},
Canonical: []string{"https://www.example.com/.well-known/security.txt"},
Contact: []string{
"mailto:[email protected]",
"tel:+1-201-555-0123",
"https://example.com/security-contact.html",
},
Enabled: true,
Encryption: []string{
"https://example.com/pgp-key.txt",
"dns:5d2d37ab76d47d36._openpgpkey.example.com?type=OPENPGPKEY",
"openpgp4fpr:5f2de5521c63a801ab59ccb603d49de44b29100f",
},
Expires: &expires,
Hiring: []string{"https://example.com/jobs.html"},
Policy: []string{"https://example.com/disclosure-policy.html"},
PreferredLanguages: "en, es, fr",
}

_, err := client.GetSecurityTXT(context.Background(), ZoneIdentifier(""))
assert.Error(t, err, "Expected error for missing Zone ID")

actual, err := client.GetSecurityTXT(context.Background(), ZoneIdentifier(testZoneID))
if assert.NoError(t, err) {
assert.Equal(t, want, actual)
}
}

func TestSecurityTXT_Update(t *testing.T) {
setup()
defer teardown()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{
"result":
{
"acknowledgments": [
"https://example.com/hall-of-fame.html"
],
"canonical": [
"https://www.example.com/.well-known/security.txt"
],
"contact": [
"mailto:[email protected]",
"tel:+1-201-555-0123",
"https://example.com/security-contact.html"
],
"enabled": false,
"encryption": [
"https://example.com/pgp-key.txt",
"dns:5d2d37ab76d47d36._openpgpkey.example.com?type=OPENPGPKEY",
"openpgp4fpr:5f2de5521c63a801ab59ccb603d49de44b29100f"
],
"expires": "2020-12-02T20:24:07.776073Z",
"hiring": [
"https://example.com/jobs.html"
],
"policy": [
"https://example.com/disclosure-policy.html"
],
"preferredLanguages": "en, es, fr"
},
"success": true,
"errors": [],
"messages": []
}`)
}

mux.HandleFunc("/zones/"+testZoneID+"/security-center/securitytxt", handler)

expires, _ := time.Parse(time.RFC3339, "2020-12-02T20:24:07.776073Z")

want := SecurityTXT{
Acknowledgements: []string{"https://example.com/hall-of-fame.html"},
Canonical: []string{"https://www.example.com/.well-known/security.txt"},
Contact: []string{
"mailto:[email protected]",
"tel:+1-201-555-0123",
"https://example.com/security-contact.html",
},
Enabled: false,
Encryption: []string{
"https://example.com/pgp-key.txt",
"dns:5d2d37ab76d47d36._openpgpkey.example.com?type=OPENPGPKEY",
"openpgp4fpr:5f2de5521c63a801ab59ccb603d49de44b29100f",
},
Expires: &expires,
Hiring: []string{"https://example.com/jobs.html"},
Policy: []string{"https://example.com/disclosure-policy.html"},
PreferredLanguages: "en, es, fr",
}

_, err := client.UpdateSecurityTXT(context.Background(), ZoneIdentifier(""), want)
assert.Error(t, err, "Expected error for missing Zone ID")

actual, err := client.UpdateSecurityTXT(context.Background(), ZoneIdentifier(testZoneID), want)
if assert.NoError(t, err) {
assert.Equal(t, want, actual)
}
}

func TestSecurityTXT_DELETE(t *testing.T) {
setup()
defer teardown()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `
{
"errors": [],
"messages": [],
"success": true
}
`)
}

mux.HandleFunc("/zones/"+testZoneID+"/security-center/securitytxt", handler)
err := client.DeleteSecurityTXT(context.Background(), ZoneIdentifier(""))
assert.Error(t, err, "Expected error for missing Zone ID")

err = client.DeleteSecurityTXT(context.Background(), ZoneIdentifier(testZoneID))
assert.NoError(t, err)
}

0 comments on commit c8e46f8

Please sign in to comment.