pax_global_header00006660000000000000000000000064137025734560014526gustar00rootroot0000000000000052 comment=9a26216947e00989d80ae39ac520af744ff40384 dmarc-cat-0.14.0/000077500000000000000000000000001370257345600134435ustar00rootroot00000000000000dmarc-cat-0.14.0/.gitignore000066400000000000000000000004161370257345600154340ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### Go template # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # goland .idea dmarc-cat-0.14.0/.travis.yml000066400000000000000000000006141370257345600155550ustar00rootroot00000000000000language: go go: - "1.12.x" - master matrix: allow_failures: - go: master fast_finish: true branches: only: - master - develop env: - GO111MODULE=on CGO_CFLAGS=-I/usr/local/include CGO_LDFLAGS=-L/usr/local/lib before_install: - sudo apt-get update -qq - sudo apt-get install -qq libgpgme11 libgpgme11-dev libassuan-dev libassuan0 libgpg-error0 gnupg2 script: - make - make test dmarc-cat-0.14.0/LICENSE.md000066400000000000000000000027421370257345600150540ustar00rootroot00000000000000Copyright (c) 2018, Ollivier Robert All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the author. dmarc-cat-0.14.0/Makefile000066400000000000000000000006501370257345600151040ustar00rootroot00000000000000# Main Makefile for dmarc-cat # # Copyright 2018 © by Ollivier Robert # GO= go GOBIN= ${GOPATH}/bin BIN= dmarc-cat SRCS= analyze.go file.go main.go resolve.go types.go utils.go OPTS= -ldflags="-s -w" -v all: ${BIN} ${BIN}: ${SRCS} ${GO} build -o ${BIN} ${OPTS} . test: ${GO} test -v . lint: gometalinter install: ${BIN} ${GO} install ${OPTS} . clean: ${GO} clean -v push: git push --all git push --tags dmarc-cat-0.14.0/README.md000066400000000000000000000050251370257345600147240ustar00rootroot00000000000000# README.md ## Status [![GitHub release](https://img.shields.io/github/release/keltia/dmarc-cat.svg)](https://github.com/keltia/dmarc-cat/releases) [![GitHub issues](https://img.shields.io/github/issues/keltia/dmarc-cat.svg)](https://github.com/keltia/dmarc-cat/issues) [![Go Version](https://img.shields.io/badge/go-1.10-blue.svg)](https://golang.org/dl/) [![Build Status](https://travis-ci.org/keltia/dmarc-cat.svg?branch=master)](https://travis-ci.org/keltia/dmarc-cat) [![GoDoc](http://godoc.org/github.com/keltia/dmarc-cat?status.svg)](http://godoc.org/github.com/keltia/dmarc-cat) [![SemVer](http://img.shields.io/SemVer/2.0.0.png)](https://semver.org/spec/v2.0.0.html) [![License](https://img.shields.io/pypi/l/Django.svg)](https://opensource.org/licenses/BSD-2-Clause) [![Go Report Card](https://goreportcard.com/badge/github.com/keltia/dmarc-cat)](https://goreportcard.com/report/github.com/keltia/dmarc-cat) ## Installation As with many Go utilities, a simple go get github.com/keltia/dmarc-cat is enough to fetch, build and install. On some systems you may need to add some environment variables to enable the Go and C compilers to find the `gpgme` include files and libraries. CGO_CFLAGS="-I/usr/local/include" CGO_LDFLAGS="-L/usr/local/lib" go get ... ## Dependencies Aside from the standard library, I use `github.com/intel/tfortools` to generate tables. go get -u github.com/intel/tfortools It also use my own module `github.com/keltia/archive` to handle the various archive types. If you use Go modules, it should all work automatically. ## Usage SYNOPSIS ``` dmarc-cat -hvD Example: $ dmarc-cat /tmp/yahoo.com\!keltia.net\!1518912000\!1518998399.xml Reporting by: Yahoo! Inc. — postmaster@dmarc.yahoo.com From 2018-02-18 01:00:00 +0100 CET to 2018-02-19 00:59:59 +0100 CET Domain: keltia.net Policy: p=none; dkim=r; spf=r Reports(1): IP Count From RFrom RDKIM RSPF 88.191.250.24 1 keltia.net keltia.net neutral pass ``` ## Tests Getting close to 90% coverage. ## License This is released under the BSD 2-Clause license. See `LICENSE.md`. ## Contributing I use Git Flow for this package so please use something similar or the usual github workflow. 1. Fork it ( https://github.com/keltia/dmarc-cat/fork ) 2. Checkout the develop branch (`git checkout develop`) 3. Create your feature branch (`git checkout -b my-new-feature`) 4. Commit your changes (`git commit -am 'Add some feature'`) 5. Push to the branch (`git push origin my-new-feature`) 6. Create a new Pull Request dmarc-cat-0.14.0/analyze.go000066400000000000000000000075341370257345600154460ustar00rootroot00000000000000package main import ( "bytes" "fmt" "sync" "text/template" "time" "github.com/intel/tfortools" "github.com/pkg/errors" ) const ( reportTmpl = `{{.MyName}} {{.MyVersion}}/j{{.Jobs}} by {{.Author}} Reporting by: {{.Org}} — {{.Email}} From {{.DateBegin}} to {{.DateEnd}} Domain: {{.Domain}} Policy: p={{.Disposition}}; dkim={{.DKIM}}; spf={{.SPF}} Reports({{.Count}}): ` rowTmpl = `{{ table (sort . %s)}}` ) // My template vars type headVars struct { MyName string MyVersion string Jobs string Author string Org string Email string DateBegin string DateEnd string Domain string Disposition string DKIM string SPF string Pct int Count int } // Entry representes a single entry type Entry struct { IP string Count int From string RFrom string RDKIM string RSPF string } type IP struct { IP string Name string } // ParallelSolve is doing the IP to name resolution with a worker set. // XXX use Mutex func ParallelSolve(ctx *Context, iplist []IP) []IP { verbose("ParallelSolve with %d workers", ctx.jobs) var lock sync.Mutex wg := &sync.WaitGroup{} queue := make(chan IP, ctx.jobs) resolved := iplist ind := 0 for i := 0; i < ctx.jobs; i++ { wg.Add(1) debug("setting up w%d", i) go func(n int, wg *sync.WaitGroup) { defer wg.Done() var name string for e := range queue { ips, err := ctx.r.LookupAddr(e.IP) debug("ips=%#v", ips) if err != nil { name = e.IP } else { name = ips[0] } lock.Lock() resolved[ind].Name = name ind++ lock.Unlock() debug("w%d - ip=%s - name=%s", n, e.IP, name) } }(i, wg) } for _, q := range iplist { queue <- q } close(queue) wg.Wait() debug("resolved=%#v", resolved) return resolved } // GatherRows extracts all IP and return the rows func GatherRows(ctx *Context, r Feedback) []Entry { var ( rows []Entry iplist []IP newlist []IP ) ipslen := len(r.Records) verbose("Resolving all %d IPs", ipslen) iplist = make([]IP, ipslen) // Get all IPs for i, report := range r.Records { iplist[i] = IP{IP: report.Row.SourceIP.String()} } // Now we have a nice array newlist = ParallelSolve(ctx, iplist) verbose("Resolved %d IPs", ipslen) for i, report := range r.Records { ip0 := newlist[i].Name current := Entry{ IP: ip0, Count: report.Row.Count, From: report.Identifiers.HeaderFrom, RSPF: report.AuthResults.SPF.Result, RDKIM: report.AuthResults.DKIM.Result, } if report.AuthResults.DKIM.Domain == "" { current.RFrom = report.AuthResults.SPF.Domain } else { current.RFrom = report.AuthResults.DKIM.Domain } rows = append(rows, current) } return rows } // Analyze extract and display what we want func Analyze(ctx *Context, r Feedback) (string, error) { var buf bytes.Buffer tmplvars := &headVars{ MyName: MyName, MyVersion: MyVersion, Jobs: fmt.Sprintf("%d", fJobs), Author: Author, Org: r.Metadata.OrgName, Email: r.Metadata.Email, DateBegin: time.Unix(r.Metadata.Date.Begin, 0).String(), DateEnd: time.Unix(r.Metadata.Date.End, 0).String(), Domain: r.Policy.Domain, Disposition: r.Policy.P, DKIM: r.Policy.ADKIM, SPF: r.Policy.ASPF, Pct: r.Policy.Pct, Count: len(r.Records), } rows := GatherRows(ctx, r) if len(rows) == 0 { return "", fmt.Errorf("empty report") } // Header t := template.Must(template.New("r").Parse(string(reportTmpl))) err := t.ExecuteTemplate(&buf, "r", tmplvars) if err != nil { return "", errors.Wrapf(err, "error in template 'r'") } // Generate our template sortTmpl := fmt.Sprintf(rowTmpl, fSort) err = tfortools.OutputToTemplate(&buf, "reports", sortTmpl, rows, nil) if err != nil { return "", errors.Wrapf(err, "error in template 'reports'") } return buf.String(), nil } dmarc-cat-0.14.0/analyze_test.go000066400000000000000000000027751370257345600165070ustar00rootroot00000000000000package main import ( "encoding/xml" "fmt" "testing" "github.com/keltia/archive" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAnalyze(t *testing.T) { ctx := &Context{NullResolver{}, 1} s, err := Analyze(ctx, Feedback{}) assert.Error(t, err) assert.Empty(t, s) } func TestGatherRows_Empty(t *testing.T) { ctx := &Context{NullResolver{}, 1} r := GatherRows(ctx, Feedback{}) assert.Empty(t, r) } func TestGatherRows_Good(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/example.com!keltia.net!1538604008!1538690408.xml" a, err := archive.New(file) require.NoError(t, err) body, err := a.Extract(".xml") require.NoError(t, err) var report Feedback err = xml.Unmarshal(body, &report) require.NoError(t, err) rows := GatherRows(ctx, report) assert.Equal(t, 1, len(rows)) } type ErrResolver struct{} func (ErrResolver) LookupAddr(ip string) ([]string, error) { return []string{"BAD"}, fmt.Errorf("fake error") } func TestParallelSolve_Error(t *testing.T) { ctx := &Context{r: ErrResolver{}, jobs: 1} td := []IP{ {IP: "8.8.8.8", Name: "BAD"}, {IP: "8.8.4.4", Name: "BAD"}, } ips := ParallelSolve(ctx, td) assert.NotEmpty(t, ips) assert.EqualValues(t, td, ips) } func TestParallelSolve_Good(t *testing.T) { ctx := &Context{r: NullResolver{}, jobs: 1} td := []IP{ {IP: "8.8.8.8", Name: "8.8.8.8"}, {IP: "8.8.4.4", Name: "8.8.4.4"}, } ips := ParallelSolve(ctx, td) assert.NotEmpty(t, ips) assert.EqualValues(t, td, ips) } dmarc-cat-0.14.0/dmarc.xsd000066400000000000000000000245661370257345600152660ustar00rootroot00000000000000 dmarc-cat-0.14.0/file.go000066400000000000000000000042351370257345600147150ustar00rootroot00000000000000package main import ( "bytes" "encoding/xml" "io" "io/ioutil" "path/filepath" "regexp" "github.com/keltia/archive" "github.com/pkg/errors" ) /* cf. https://tools.ietf.org/html/rfc7489#section-7.2.1.1 filename = receiver "!" policy-domain "!" begin-timestamp "!" end-timestamp [ "!" unique-id ] "." extension unique-id = 1*(ALPHA / DIGIT) */ const ( reFileName = `^([\S\.]+)!([\S\.]+)!([\d]+)!([\d]+)(![[:alnum:]]+)*(\.\S+)(\.(gz|zip))*$` ) var reFN *regexp.Regexp func init() { reFN = regexp.MustCompile(reFileName) } func checkFilename(file string) (ok bool) { base := filepath.Base(file) return reFN.MatchString(base) } // HandleZipFile is here for zip files because archive.NewFromReader() does not work here func HandleZipFile(ctx *Context, file string) (string, error) { debug("HandleZipFile") var body []byte a, err := archive.New(file) if err == nil { body, err = a.Extract(".xml") if err != nil { return "", errors.Wrap(err, "extract") } } else { // Got plain text (i.e. xml) if body, err = ioutil.ReadFile(file); err != nil { return "", errors.Wrap(err, "ReadFile") } } debug("xml=%s", string(body)) var report Feedback if err := xml.Unmarshal(body, &report); err != nil { return "", errors.Wrap(err, "unmarshall") } debug("report=%v\n", report) return Analyze(ctx, report) } // HandleSingleFile creates a tempdir and dispatch csv/zip files to handler. func HandleSingleFile(ctx *Context, r io.ReadCloser, typ int) (string, error) { debug("HandleSingleFile") var body []byte debug("typ=%d", typ) if typ == archive.ArchiveZip { return "", errors.New("unsupported") } a, err := archive.NewFromReader(r, typ) if err == nil { debug("a=%#v", a) body, err = a.Extract("") if err != nil { return "", errors.Wrap(err, "extract") } } else { // Got plain text (i.e. xml) buf := bytes.NewBuffer(body) _, err := io.Copy(buf, r) if err != nil { return "", errors.Wrap(err, "copy") } } debug("xml=%#v", body) var report Feedback if err := xml.Unmarshal(body, &report); err != nil { return "", errors.Wrap(err, "unmarshall") } debug("report=%v\n", report) return Analyze(ctx, report) } dmarc-cat-0.14.0/file_test.go000066400000000000000000000110601370257345600157460ustar00rootroot00000000000000package main import ( "os" "path/filepath" "testing" "github.com/keltia/archive" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCheckFilename(t *testing.T) { td := []struct { In string Out bool }{ {"foo.bar", false}, {"example.com!keltia.net!1538604008!1538690408.xml.gz", true}, {"example.com!keltia.net!1538604008!1538690408.xml.gz", true}, {"example.com!keltia.net!1538604008!1538690408.xml", true}, {"example.com!keltia.net!1538604008!1538690408!666.xml", true}, {"google.com!keltia.net!1538438400!1538524799.zip", true}, } for _, e := range td { res := checkFilename(e.In) assert.Equal(t, e.Out, res, e.In) } } func TestHandleZipFile(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/google.com!keltia.net!1538438400!1538524799.zip" txt, err := HandleZipFile(ctx, file) assert.NoError(t, err) assert.NotEmpty(t, txt) } func TestHandleZipFile_Xml(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/example.com!keltia.net!1538604008!1538690408.xml" txt, err := HandleZipFile(ctx, file) assert.NoError(t, err) assert.NotEmpty(t, txt) } func TestHandleZipFile_Bad(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/notempty.zip" txt, err := HandleZipFile(ctx, file) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleZipFile_Bad1(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/bad.zip" txt, err := HandleZipFile(ctx, file) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleZipFile_None(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "/nonexistent" txt, err := HandleZipFile(ctx, file) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Plain(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/empty.txt" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Gzip(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/example.com!keltia.net!1538604008!1538690408.xml.gz" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(".gz")) assert.NoError(t, err) assert.NotEmpty(t, txt) } func TestHandleSingleFile_Zip(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/google.com!keltia.net!1538438400!1538524799.zip" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Xml(t *testing.T) { ctx := &Context{NullResolver{}, 1} fDebug = true file := "testdata/example.com!keltia.net!1538604008!1538690408.xml" fh, err := os.Open(file) require.NoError(t, err) assert.Equal(t, archive.ArchivePlain, archive.Ext2Type(filepath.Ext(file))) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.NoError(t, err) assert.NotEmpty(t, txt) fDebug = false } func TestHandleSingleFile_Null(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "/dev/null" fh, err := os.Open(file) require.NoError(t, err) assert.Equal(t, archive.ArchivePlain, archive.Ext2Type(filepath.Ext(file))) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Txt(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/bad.xml" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, 255) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_TxtNull(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "/dev/null" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, 255) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Verbose(t *testing.T) { fVerbose = true ctx := &Context{NullResolver{}, 1} file := "testdata/empty.txt" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) fVerbose = false } func TestHandleSingleFile_Debug(t *testing.T) { fDebug = true ctx := &Context{NullResolver{}, 1} file := "testdata/empty.txt" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) fDebug = false } dmarc-cat-0.14.0/go.mod000066400000000000000000000004141370257345600145500ustar00rootroot00000000000000module github.com/keltia/dmarc-cat require ( github.com/intel/tfortools v0.2.0 github.com/keltia/archive v0.7.0 github.com/pkg/errors v0.8.1 github.com/proglottis/gpgme v0.0.0-20190226023825-8e0937a489db // indirect github.com/stretchr/testify v1.3.0 ) go 1.13 dmarc-cat-0.14.0/go.sum000066400000000000000000000030461370257345600146010ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/intel/tfortools v0.2.0/go.mod h1:VNwPPab3wzbXX9CBtgsD718qK1w6ryEydP6ZoIrbI7w= github.com/keltia/archive v0.7.0 h1:try3Jz2eEy1C5dyaT5SX0vrQhSv83GF6W/Uyrn3rjJk= github.com/keltia/archive v0.7.0/go.mod h1:/TpH+TDytTz3djAzR3z5QfBGhCKEuGbbE/cl/uHPF40= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/proglottis/gpgme v0.0.0-20181127053519-3b0be0916cd5 h1:eRA7dSSk5ag/eeeHd8JzDhP5v9LYfzA4DnRSpKPl/Ak= github.com/proglottis/gpgme v0.0.0-20181127053519-3b0be0916cd5/go.mod h1:hbKCks+19s4oK5vcPKxliXTANhPsfz972l5GVM5+FYE= github.com/proglottis/gpgme v0.0.0-20190226023825-8e0937a489db h1:rZhGqKvKPpjnTMVhIXeRMeSP82/gAD1N50lkAGXZJBc= github.com/proglottis/gpgme v0.0.0-20190226023825-8e0937a489db/go.mod h1:hbKCks+19s4oK5vcPKxliXTANhPsfz972l5GVM5+FYE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= dmarc-cat-0.14.0/main.go000066400000000000000000000054461370257345600147270ustar00rootroot00000000000000package main import ( "flag" "fmt" "io" "log" "os" "path/filepath" "runtime" "github.com/keltia/archive" "github.com/pkg/errors" ) var ( // MyName is the application MyName = filepath.Base(os.Args[0]) // MyVersion is our version MyVersion = "0.14.0,parallel" // Author should be obvious Author = "Ollivier Robert" fDebug bool fJobs int fNoResolv bool fSort string fType string fVerbose bool fVersion bool ) // Context is passed around rather than being a global var/struct type Context struct { r Resolver jobs int } func init() { flag.BoolVar(&fDebug, "D", false, "Debug mode") flag.BoolVar(&fNoResolv, "N", false, "Do not resolve IPs") flag.IntVar(&fJobs, "j", runtime.NumCPU(), "Parallel jobs") flag.StringVar(&fSort, "S", `"Count" "dsc"`, "Sort results") flag.StringVar(&fType, "t", "", "File type for stdin mode") flag.BoolVar(&fVerbose, "v", false, "Verbose mode") flag.BoolVar(&fVersion, "version", false, "Display version") } func Version() { fmt.Printf("%s version %s/j%d archive/%s\n", MyName, MyVersion, fJobs, archive.Version()) } // Setup creates our context and check stuff func Setup(a []string) (*Context, error) { // Exist early if -version if fVersion { Version() return nil, nil } if fDebug { fVerbose = true debug("debug mode") } if len(a) < 1 { return nil, fmt.Errorf("You must specify at least one file.") } ctx := &Context{RealResolver{}, fJobs} // Make it easier to sub it out if fNoResolv { ctx.r = NullResolver{} } return ctx, nil } func SelectInput(file string) (io.ReadCloser, error) { debug("file=%s", file) if file == "-" { if fType == "" { return nil, errors.New("Wrong file type, use -t") } return os.Stdin, nil } // We have a filename if !checkFilename(file) { return nil, errors.New("bad filename") } // We want the full path myfile, err := filepath.Abs(file) if err != nil { return nil, errors.Wrapf(err, "Abs(%s)", file) } return os.Open(myfile) } func realmain(args []string) error { flag.Parse() ctx, err := Setup(args) if ctx == nil { return errors.Wrap(err, "realmain") } var txt string // Look for input file or stdin/"-" file := args[0] verbose("Analyzing %s", file) if filepath.Ext(file) == ".zip" || filepath.Ext(file) == ".ZIP" { txt, err = HandleZipFile(ctx, file) if err != nil { return errors.Wrapf(err, "file %s:", file) } } else { in, err := SelectInput(file) if err != nil { return errors.Wrap(err, "SelectInput") } defer in.Close() typ := archive.Ext2Type(fType) txt, err = HandleSingleFile(ctx, in, typ) if err != nil { return errors.Wrapf(err, "file %s:", file) } } fmt.Println(txt) return nil } func main() { // Parse CLI flag.Parse() if err := realmain(flag.Args()); err != nil { log.Fatalf("Error: %v\n", err) } } dmarc-cat-0.14.0/main_test.go000066400000000000000000000044451370257345600157640ustar00rootroot00000000000000package main import ( "os" "testing" "github.com/stretchr/testify/assert" ) func TestSetup(t *testing.T) { ctx, err := Setup([]string{}) assert.Nil(t, ctx) assert.Error(t, err) } func TestSetup2(t *testing.T) { ctx, err := Setup([]string{"foo.zip"}) assert.NotNil(t, ctx) assert.NoError(t, err) assert.IsType(t, (*Context)(nil), ctx) } func TestSetup3(t *testing.T) { fNoResolv = true ctx, err := Setup([]string{"foo.zip"}) assert.NotNil(t, ctx) assert.NoError(t, err) assert.IsType(t, (*Context)(nil), ctx) fNoResolv = false } func TestSetup4(t *testing.T) { fVersion = true ctx, err := Setup([]string{"foo.zip"}) assert.Nil(t, ctx) assert.NoError(t, err) fVersion = false } func TestVersion(t *testing.T) { Version() } func TestSelectInput_Bad(t *testing.T) { _, err := SelectInput("-") assert.Error(t, err) } func TestSelectInput_Good(t *testing.T) { fType = ".xml" r, err := SelectInput("-") assert.NoError(t, err) assert.EqualValues(t, os.Stdin, r) fType = "" } func TestSelectInput_Badfn(t *testing.T) { _, err := SelectInput("/testdata/google.com!keltia.net!1538438400!1538524798.xml") assert.Error(t, err) } // realmain() is the thing now func TestMain_Noargs(t *testing.T) { assert.Error(t, realmain([]string{})) } func TestMain_Noargs_Verbose(t *testing.T) { fVerbose = true assert.Error(t, realmain([]string{})) fVerbose = false } func TestMain_Noargs_Debug(t *testing.T) { fDebug = true assert.Error(t, realmain([]string{})) fDebug = false } func TestMain_Noargs_NoResolv(t *testing.T) { fNoResolv = true r := realmain([]string{"testdata/google.com!keltia.net!1538438400!1538524799.xml"}) fNoResolv = false assert.Empty(t, r) } func TestRealmain_GoodFile(t *testing.T) { r := realmain([]string{"testdata/google.com!keltia.net!1538438400!1538524799.xml"}) assert.Empty(t, r) } func TestRealmain_GoodFile1(t *testing.T) { r := realmain([]string{"testdata/google.com!keltia.net!1538438400!1538524799.zip"}) assert.Empty(t, r) } func TestRealmain_NoFile(t *testing.T) { r := realmain([]string{"/nonexistent"}) assert.NotEmpty(t, r) } func TestMain_EmptyArg(t *testing.T) { r := realmain([]string{"foo"}) assert.NotEmpty(t, r) } func TestMain_GoodFile(t *testing.T) { os.Args = append(os.Args, "testdata/google.com!keltia.net!1538438400!1538524799.zip") main() } dmarc-cat-0.14.0/resolve.go000066400000000000000000000010501370257345600154450ustar00rootroot00000000000000package main import ( "net" ) // Resolver is the main interface we use type Resolver interface { LookupAddr(addr string) ([]string, error) } // NullResolver is empty type NullResolver struct{} // LookupAddr always return a good and fixed answer func (NullResolver) LookupAddr(addr string) ([]string, error) { return []string{addr}, nil } // RealResolver will call the real one type RealResolver struct{} // LookupAddr use the real "net" function func (r RealResolver) LookupAddr(addr string) ([]string, error) { return net.LookupAddr(addr) } dmarc-cat-0.14.0/resolve_test.go000066400000000000000000000006751370257345600165200ustar00rootroot00000000000000package main import ( "testing" "github.com/stretchr/testify/assert" ) func TestNullResolver_LookupAddr(t *testing.T) { var r NullResolver resp, err := r.LookupAddr("example.com") assert.NoError(t, err) assert.Equal(t, []string{"example.com"}, resp) } func TestRealResolver_LookupAddr(t *testing.T) { var r RealResolver resp, err := r.LookupAddr("8.8.8.8") assert.NoError(t, err) assert.Equal(t, []string{"dns.google."}, resp) } dmarc-cat-0.14.0/testdata/000077500000000000000000000000001370257345600152545ustar00rootroot00000000000000dmarc-cat-0.14.0/testdata/bad.xml000066400000000000000000000000001370257345600165120ustar00rootroot00000000000000dmarc-cat-0.14.0/testdata/bad.zip000066400000000000000000000002441370257345600165260ustar00rootroot00000000000000PK ;mHMbad.xmlUT B[\ux PK ;mHMbad.xmlUTB[ux PKMAdmarc-cat-0.14.0/testdata/empty.txt000066400000000000000000000000001370257345600171410ustar00rootroot00000000000000dmarc-cat-0.14.0/testdata/example.com!keltia.net!1538604008!1538690408.xml000066400000000000000000000022411370257345600243460ustar00rootroot00000000000000 1.0 esa1.eurocontrol.c3s2.iphmx.com MAILER-DAEMON@esa1.eurocontrol.c3s2.iphmx.com 1a6ffb$9f77bea=6b2f4786a9824806@esa1.eurocontrol.c3s2.iphmx.com 1538604008 1538690408 keltia.net r r

none

100
88.191.250.24 1 none fail pass keltia.net keltia.net keltia.net mfrom pass
dmarc-cat-0.14.0/testdata/example.com!keltia.net!1538604008!1538690408.xml.gz000066400000000000000000000011211370257345600247610ustar00rootroot00000000000000u[esa1.eurocontrol.c3s2.iphmx.com!keltia.net!1538604008!1538690408.xmlT]o0 |i8@;`l2%Aߏl'Y #ϴӡm&{NjI6⺖jNuM&StB‚֗-xV3R(8'Y͵V7 pDm{HnI:2Ehl˷o/o?<߬/޲20TBS$\w/k՗X.+`E5e`b6/&NbitJԦ 6-||(< Fdȃcvĭ8K/j06EwxE(Y-$tFJ+ cgY7D{gplvpЙٝ,RZIʓc0&*םB1zgMnC"X Nqs LH0bL?3LFd K!m`Kau{ZNٗ^JoK k!noHQA =8d,G J%>tY4 dmarc-cat-0.14.0/testdata/google.com!keltia.net!1538438400!1538524799.xml000066400000000000000000000027611370257345600242100ustar00rootroot00000000000000 google.com noreply-dmarc-support@google.com https://support.google.com/a/answer/2466580 15591417298178277408 1538438400 1538524799 keltia.net r r

none

none 100
195.154.227.159 1 none fail fail keltia.net example.org pass 217.70.183.200 1 none fail fail keltia.net example.org pass
dmarc-cat-0.14.0/testdata/google.com!keltia.net!1538438400!1538524799.zip000066400000000000000000000013511370257345600242040ustar00rootroot00000000000000PKZFM@x/google.com!keltia.net!1538438400!1538524799.xmlUT *[*[ux TMs0Wx|72(JO홑ł5I#]񑤇{ 3o? r+X'zާq%t#Uǡ^m3WQ F[_y=vN뮇D聒|`gJ#C~hn&}߆M~1-V _Kjv޸od %p,z*#r}caiQ<-SUVdO+Ԗ.v:09b tZLF>-ٶCFRf<]`)DxB%OxdΛd:DЙTB%&~p3bgih+e$v귥G+"I<ɲ84; =*LGt^y?̚!Nz\X1RL$F2MrHsoT6l%^%[VK8壿qSԪ;`pn}>nu`;jO> γ#hIyL1†_ՓPKZFM@x/google.com!keltia.net!1538438400!1538524799.xmlUT*[ux PKu^dmarc-cat-0.14.0/testdata/notempty.txt000066400000000000000000000000171370257345600176720ustar00rootroot00000000000000this is a file dmarc-cat-0.14.0/testdata/notempty.txt.gz000066400000000000000000000000561370257345600203140ustar00rootroot00000000000000S[notempty.txt+,VD̜T.Pfdmarc-cat-0.14.0/testdata/notempty.zip000066400000000000000000000002751370257345600176630ustar00rootroot00000000000000PK ׉HMPf notempty.txtUT ft[z[ux this is a file PK ׉HMPf notempty.txtUTft[ux PKRUdmarc-cat-0.14.0/types.go000066400000000000000000000044321370257345600151410ustar00rootroot00000000000000package main import ( "net" ) // DateRange time period type DateRange struct { Begin int64 `xml:"begin"` End int64 `xml:"end"` } // ReportMetadata for the report type ReportMetadata struct { OrgName string `xml:"org_name"` Email string `xml:"email"` ExtraContactInfo string `xml:"extra_contact_info"` ReportID string `xml:"report_id"` Date DateRange `xml:"date_range"` Errors []string `xml:"error"` } // PolicyPublished found in DNS type PolicyPublished struct { Domain string `xml:"domain"` ADKIM string `xml:"adkim"` ASPF string `xml:"aspf"` P string `xml:"p"` SP string `xml:"sp"` Pct int `xml:"pct"` Fo string `xml:"fo"` } // PolicyEvaluated what was evaluated type PolicyEvaluated struct { Disposition string `xml:"disposition"` DKIM string `xml:"dkim"` SPF string `xml:"spf"` Reasons []PolicyOverrideReason `xml:"reason,omitempty"` } // PolicyOverrideReason are the reasons that may affect DMARC disposition // or execution thereof type PolicyOverrideReason struct { Type string `xml:"type"` Comment string `xml:"comment"` } // Row for each IP address type Row struct { SourceIP net.IP `xml:"source_ip"` Count int `xml:"count"` Policy PolicyEvaluated `xml:"policy_evaluated"` } // Identifiers headers checked type Identifiers struct { HeaderFrom string `xml:"header_from"` EnvelopeFrom string `xml:"envelope_from"` EnvelopeTo string `xml:"envelope_to,omitempty"` } // Result for each IP type Result struct { Domain string `xml:"domain"` Selector string `xml:"selector"` Result string `xml:"result"` HumanResult string `xml:"human_result"` } // AuthResults for DKIM/SPF type AuthResults struct { DKIM Result `xml:"dkim,omitempty"` SPF Result `xml:"spf,omitempty"` } // Record for each IP type Record struct { Row Row `xml:"row"` Identifiers Identifiers `xml:"identifiers"` AuthResults AuthResults `xml:"auth_results"` } // Feedback the report itself type Feedback struct { Version float32 `xml:"version"` Metadata ReportMetadata `xml:"report_metadata"` Policy PolicyPublished `xml:"policy_published"` Records []Record `xml:"record"` } dmarc-cat-0.14.0/utils.go000066400000000000000000000004441370257345600151340ustar00rootroot00000000000000package main import ( "log" ) // debug displays only if fDebug is set func debug(str string, a ...interface{}) { if fDebug { log.Printf(str, a...) } } // verbose displays only if fVerbose is set func verbose(str string, a ...interface{}) { if fVerbose { log.Printf(str, a...) } } dmarc-cat-0.14.0/utils_test.go000066400000000000000000000005001370257345600161640ustar00rootroot00000000000000package main import ( "testing" ) func TestVerbose_No(t *testing.T) { verbose("no") } func TestVerbose_Yes(t *testing.T) { fVerbose = true verbose("yes") fVerbose = false } func TestDebug_No(t *testing.T) { debug("no") } func TestDebug_Yes(t *testing.T) { fVerbose = true verbose("yes") fVerbose = false }