mirror of
https://github.com/foomo/securitytxt.git
synced 2025-10-16 12:35:42 +00:00
init
This commit is contained in:
parent
b2bd40a40c
commit
564f6d6e3f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
config
|
||||
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/securitytxt.iml" filepath="$PROJECT_DIR$/.idea/securitytxt.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/securitytxt.iml
Normal file
9
.idea/securitytxt.iml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Phil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module github.com/dreadl0ck/securitytxt
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
17
middleware.go
Normal file
17
middleware.go
Normal file
@ -0,0 +1,17 @@
|
||||
package securitytxt
|
||||
|
||||
import "net/http"
|
||||
import "go.uber.org/zap"
|
||||
|
||||
func Middleware() func(l *zap.Logger, name string, next http.Handler) http.Handler {
|
||||
h := Handler()
|
||||
return func(l *zap.Logger, name string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/.well-known/security.txt" {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
108
securitytxt.go
Normal file
108
securitytxt.go
Normal file
@ -0,0 +1,108 @@
|
||||
package securitytxt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: http handler
|
||||
// generate security txt once on startup
|
||||
// zero external deps
|
||||
// digital signing
|
||||
// PGP key
|
||||
|
||||
type Date struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
//func (t *Date) UnmarshalText(text []byte) error {
|
||||
// tt, err := time.Parse("2006-01-02", string(text))
|
||||
// if err != nil {
|
||||
// tt, err = time.Parse(time.RFC3339, string(text))
|
||||
// }
|
||||
// *t = Date{tt}
|
||||
// return err
|
||||
//}
|
||||
|
||||
type config struct {
|
||||
Expires string
|
||||
Comment string
|
||||
|
||||
Contact string
|
||||
Acknowledgments string
|
||||
Canonical string
|
||||
Encryption string
|
||||
Hiring string
|
||||
PreferredLanguages []string
|
||||
Policy string
|
||||
CSAF string
|
||||
}
|
||||
|
||||
func getStringsEnv(key string) []string {
|
||||
return strings.Split(os.Getenv(key), " ")
|
||||
}
|
||||
|
||||
func Handler() http.HandlerFunc {
|
||||
|
||||
cfg := config{
|
||||
Comment: os.Getenv("COMMENT"),
|
||||
Expires: os.Getenv("EXPIRES"),
|
||||
Contact: os.Getenv("CONTACT"),
|
||||
Acknowledgments: os.Getenv("ACKNOWLEDGMENT"),
|
||||
Canonical: os.Getenv("CANONICAL"),
|
||||
Encryption: os.Getenv("ENCRYPTION"),
|
||||
Hiring: os.Getenv("HIRING"),
|
||||
PreferredLanguages: getStringsEnv("PREFERRED_LANGUAGES"),
|
||||
Policy: os.Getenv("POLICY"),
|
||||
CSAF: os.Getenv("CSAF"),
|
||||
}
|
||||
|
||||
data, err := createSecurityTxt(cfg)
|
||||
if err != nil {
|
||||
fmt.Println("could not load required values:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func createSecurityTxt(c config) ([]byte, error) {
|
||||
|
||||
if len(c.Contact) == 0 {
|
||||
return nil, errors.New("contact must be provided")
|
||||
}
|
||||
|
||||
var out []string
|
||||
|
||||
field := func(prefix string, body string) string {
|
||||
return fmt.Sprintf("%s %s", prefix, body)
|
||||
}
|
||||
|
||||
if len(c.Comment) > 0 {
|
||||
out = append(out, field("#", c.Comment))
|
||||
}
|
||||
|
||||
out = append(out, field("Contact:", c.Contact))
|
||||
out = append(out, fmt.Sprintf("Expires: %s", c.Expires))
|
||||
out = append(out, field("Encryption:", c.Encryption))
|
||||
out = append(out, field("Acknowledgments:", c.Acknowledgments))
|
||||
if len(c.PreferredLanguages) > 0 {
|
||||
out = append(out, fmt.Sprintf("Preferred-Languages: %s", strings.Join(c.PreferredLanguages, " ")))
|
||||
}
|
||||
out = append(out, field("Canonical:", c.Canonical))
|
||||
out = append(out, field("Policy:", c.Policy))
|
||||
out = append(out, field("Hiring:", c.Hiring))
|
||||
|
||||
if len(c.CSAF) > 0 {
|
||||
out = append(out, field("CSAF:", c.CSAF))
|
||||
}
|
||||
|
||||
return []byte(strings.Join(out, "\n")), nil
|
||||
}
|
||||
66
securitytxt_test.go
Normal file
66
securitytxt_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package securitytxt_test
|
||||
|
||||
import (
|
||||
"github.com/dreadl0ck/securitytxt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
|
||||
// configure via env
|
||||
assert.NoError(t, os.Setenv("EXPIRES", "2025-03-26T11:00:00.000Z"))
|
||||
assert.NoError(t, os.Setenv("COMMENT", "this is a comment"))
|
||||
assert.NoError(t, os.Setenv("CONTACT", "mailto:security@org.com"))
|
||||
assert.NoError(t, os.Setenv("ACKNOWLEDGMENT", "https://example.com/halloffame"))
|
||||
assert.NoError(t, os.Setenv("CANONICAL", "https://example.com/canonical"))
|
||||
assert.NoError(t, os.Setenv("ENCRYPTION", "https://example.com/pgpkey.txt"))
|
||||
assert.NoError(t, os.Setenv("HIRING", "https://example.com/hiring"))
|
||||
assert.NoError(t, os.Setenv("PREFERRED_LANGUAGES", "en, de"))
|
||||
assert.NoError(t, os.Setenv("POLICY", "https://example.com/policy"))
|
||||
assert.NoError(t, os.Setenv("CSAF", "https://example.com/csaf"))
|
||||
|
||||
// Create a request
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Create a ResponseRecorder to capture the response
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Serve the request
|
||||
securitytxt.Handler().ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
// Check the response body
|
||||
expected := `# this is a comment
|
||||
Contact: mailto:security@org.com
|
||||
Expires: 2025-03-26T11:00:00.000Z
|
||||
Encryption: https://example.com/pgpkey.txt
|
||||
Acknowledgments: https://example.com/halloffame
|
||||
Preferred-Languages: en, de
|
||||
Canonical: https://example.com/canonical
|
||||
Policy: https://example.com/policy
|
||||
Hiring: https://example.com/hiring
|
||||
CSAF: https://example.com/csaf`
|
||||
|
||||
body, _ := io.ReadAll(rr.Body)
|
||||
//if strings.TrimSpace(string(body)) != expected {
|
||||
// t.Errorf("Expected body %s but got %s", expected, body)
|
||||
//}
|
||||
assert.Equal(t, expected, string(body))
|
||||
|
||||
// Check content type
|
||||
if contentType := rr.Header().Get("Content-Type"); contentType != "text/plain" {
|
||||
t.Errorf("Expected content type application/json but got %s", contentType)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user