├── var
├── .gitkeep
├── http
│ └── serve
│ │ └── .gitkeep
└── https
│ ├── certs
│ └── .gitkeep
│ └── serve
│ └── .gitkeep
├── docs
├── docs
│ ├── use_cases.md
│ ├── links.md
│ ├── features.md
│ ├── index.md
│ ├── requirements.md
│ ├── installation.md
│ ├── meloctl.md
│ ├── layers.md
│ └── rules.md
└── mkdocs.yml
├── rules
├── rules-enabled
│ └── .gitkeep
└── rules-available
│ ├── web.yml
│ ├── nas.yml
│ ├── rdp.yml
│ ├── microsoft.yml
│ ├── cms.yml
│ ├── vpn.yml
│ ├── server.yml
│ └── router.yml
├── filter.bpf
├── .dockerignore
├── readme
├── melody_demo.gif
└── melody_demo_dash.png
├── internal
├── rules
│ ├── test_resources
│ │ ├── ip_values.pcap
│ │ ├── http_values.pcap
│ │ ├── logic_flow.pcap
│ │ ├── port_values.pcap
│ │ ├── tcp_values.pcap
│ │ ├── udp_values.pcap
│ │ ├── icmpv4_values.pcap
│ │ ├── icmpv6_values.pcap
│ │ ├── port_rules.yml
│ │ ├── ip_rules.yml
│ │ ├── icmpv4_rules.yml
│ │ ├── icmpv6_rules.yml
│ │ ├── udp_rules.yml
│ │ ├── tcp_rules.yml
│ │ ├── http_rules.yml
│ │ └── logic_flow_rules.yml
│ ├── rules.go
│ ├── flags_conditions.go
│ ├── tagparser.go
│ ├── load.go
│ ├── testing.go
│ ├── conditions_test.go
│ ├── conditions.go
│ └── raw_rules.go
├── loggable
│ └── loggable.go
├── meloctl
│ └── prompt
│ │ ├── validators.go
│ │ └── prompt.go
├── fileutils
│ └── fileutils.go
├── tagparser
│ └── tagparser.go
├── logdata
│ ├── udp.go
│ ├── payload.go
│ ├── tcp.go
│ ├── icmpv6.go
│ ├── icmpv4.go
│ ├── http.go
│ ├── ipv6.go
│ ├── base.go
│ └── ipv4.go
├── events
│ ├── event.go
│ ├── helpers
│ │ └── layers.go
│ ├── icmpv4.go
│ ├── base.go
│ ├── icmpv6.go
│ ├── tcp.go
│ ├── udp.go
│ └── http.go
├── clihelper
│ └── multistring.go
├── config
│ └── tagparser.go
├── router
│ ├── fs.go
│ ├── router.go
│ └── middlewares.go
├── sessions
│ └── sessions.go
├── assembler
│ └── stream.go
├── engine
│ └── engine.go
├── filters
│ ├── utils.go
│ └── portrules.go
├── logging
│ ├── control.go
│ └── loggers.go
├── httpparser
│ └── httpparser.go
└── sensor
│ └── sensor.go
├── .gitignore
├── cmd
├── meloctl
│ ├── main.go
│ ├── config.go
│ ├── get.go
│ ├── init.go
│ ├── set.go
│ └── root.go
└── melody
│ └── main.go
├── etc
├── melody.conf
└── melody.service
├── .golangci.yml
├── docker-compose.yml
├── Dockerfile
├── .github
└── workflows
│ ├── release.yml
│ └── ci.yml
├── .goreleaser.yml
├── go.mod
├── LICENSE
├── Makefile
├── config.yml
└── README.md
/var/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/docs/use_cases.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/var/http/serve/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rules/rules-enabled/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/var/https/certs/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/var/https/serve/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/filter.bpf:
--------------------------------------------------------------------------------
1 | inbound and not net 127.0.0.0/24
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | docs/
2 | logs/
3 | readme/
4 | .git/
5 | .github/
6 |
--------------------------------------------------------------------------------
/readme/melody_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/readme/melody_demo.gif
--------------------------------------------------------------------------------
/readme/melody_demo_dash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/readme/melody_demo_dash.png
--------------------------------------------------------------------------------
/docs/docs/links.md:
--------------------------------------------------------------------------------
1 | + The project is now archived in the [Github repository](https://github.com/ma111e/melody)
2 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/ip_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/ip_values.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/http_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/http_values.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/logic_flow.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/logic_flow.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/port_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/port_values.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/tcp_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/tcp_values.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/udp_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/udp_values.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/icmpv4_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/icmpv4_values.pcap
--------------------------------------------------------------------------------
/internal/rules/test_resources/icmpv6_values.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ma111e/melody/HEAD/internal/rules/test_resources/icmpv6_values.pcap
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | ./melody
3 | melody.exe
4 | logs/
5 | docs/site
6 | !*/.gitkeep
7 | var/http/serve/*
8 | var/https/serve/*
9 | var/https/certs/*
10 |
--------------------------------------------------------------------------------
/cmd/meloctl/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | func main() {
9 | if err := RootCmd.Execute(); err != nil {
10 | fmt.Println(err)
11 | os.Exit(1)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/etc/melody.conf:
--------------------------------------------------------------------------------
1 | [program:melody]
2 | command=/opt/melody/melody
3 | directory=/opt/melody
4 | stdout_logfile=/opt/melody/melody_supervisor.out
5 | stderr_logfile=/opt/melody/melody_supervisor.err
6 | autostart=true
7 | autorestart=true
8 |
--------------------------------------------------------------------------------
/etc/melody.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Melody sensor
3 | After=network-online.target
4 |
5 | [Service]
6 | Type=simple
7 | WorkingDirectory=/opt/melody
8 | ExecStart=/opt/melody/melody
9 | Restart=on-failure
10 |
11 | [Install]
12 | WantedBy=multi-user.target
13 |
--------------------------------------------------------------------------------
/internal/loggable/loggable.go:
--------------------------------------------------------------------------------
1 | package loggable
2 |
3 | // Loggable is an interface to allow mutual use of events.BaseEvent for logdata.BaseLogData
4 | type Loggable interface {
5 | GetSession() string
6 | GetTags() map[string][]string
7 | GetKind() string
8 | GetSourceIP() string
9 | GetDestPort() uint16
10 | }
11 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | skip-dirs:
3 | - docs
4 | - .github
5 |
6 | linters-settings:
7 | gocyclo:
8 | min-complexity: 15
9 | gofmt:
10 | simplify: true
11 | misspell:
12 | locale: US
13 |
14 | linters:
15 | enable:
16 | - gofmt
17 | - golint
18 | - misspell
19 | # - gocyclo
20 | disable:
21 | - errcheck
22 | disable-all: false
23 | fast: false
24 |
25 | issues:
26 | exclude-use-default: false
27 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | melody:
4 | build: .
5 | restart: always
6 | network_mode: "host"
7 | ports:
8 | - "10080:10080"
9 | - "10443:10443"
10 | environment:
11 | - MELODY_CLI=${MELODY_CLI}
12 | volumes:
13 | - ./filter.bpf:/app/filter.bpf:ro
14 | - ./config.yml:/app/config.yml:ro
15 | - ./var:/app/var:ro
16 | - ./rules:/app/rules:ro
17 | - ./logs:/app/logs
18 |
--------------------------------------------------------------------------------
/rules/rules-available/web.yml:
--------------------------------------------------------------------------------
1 | Dropper:
2 | layer: tcp
3 | meta:
4 | id: 00831c4f-17ec-4221-a2e8-85e1b146c35e
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "Attempt to exploit a vulnerability to drop a file on the system"
11 | match:
12 | tcp.payload:
13 | contains:
14 | - "wget"
15 | tags:
16 | profile: "dropper"
17 | data: "drop_server"
18 |
--------------------------------------------------------------------------------
/internal/meloctl/prompt/validators.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | var (
8 | validatorsMap = map[string]func(candidate string) error{
9 | "date": checkDate,
10 | }
11 |
12 | // DateFormat is the format used to validate dates inputs from the user
13 | DateFormat = "2006/01/02"
14 | )
15 |
16 | func checkDate(candidate string) error {
17 | _, err := time.Parse(DateFormat, candidate)
18 | if err != nil {
19 | return err
20 | }
21 |
22 | return nil
23 | }
24 |
--------------------------------------------------------------------------------
/internal/fileutils/fileutils.go:
--------------------------------------------------------------------------------
1 | package fileutils
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | // Exists check if a given file exists
8 | func Exists(filepath string) (bool, error) {
9 | var err error
10 | ok := true
11 |
12 | if f, openError := os.Open(filepath); openError != nil {
13 | _ = f.Close()
14 | // Return an error only if it is not a "not exist" error
15 | if os.IsNotExist(openError) {
16 | ok = false
17 | } else {
18 | err = openError
19 | }
20 |
21 | }
22 |
23 | return ok, err
24 | }
25 |
--------------------------------------------------------------------------------
/internal/tagparser/tagparser.go:
--------------------------------------------------------------------------------
1 | package tagparser
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/fatih/structtag"
7 | )
8 |
9 | // ParseYamlTagValue parses a tag to retrieve the value of its yaml key
10 | func ParseYamlTagValue(tag reflect.StructTag) (string, error) {
11 | parsedRuleTags, err := structtag.Parse(string(tag))
12 | if err != nil {
13 | return "", err
14 | }
15 |
16 | yamlTag, err := parsedRuleTags.Get("yaml")
17 | if err != nil {
18 | return "", err
19 | }
20 | return yamlTag.Value(), nil
21 | }
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine as base
2 |
3 | # Install libpcap headers and the compilation tools needed
4 | RUN apk add --update --no-cache ca-certificates libpcap-dev build-base
5 |
6 | WORKDIR /app
7 |
8 | ENV GO111MODULE=on \
9 | CGO_ENABLED=1 \
10 | GOOS=linux \
11 | GOARCH=amd64
12 |
13 | COPY go.mod .
14 | COPY go.sum .
15 |
16 | RUN go mod download
17 |
18 | COPY . .
19 |
20 | RUN go build -ldflags="-s -w -extldflags '-static'" -o /app/melody ./cmd/melody
21 |
22 | # Copy only what's needed
23 | FROM scratch
24 | COPY --from=base /app /app
25 | WORKDIR /app
26 |
27 | ENTRYPOINT ["/app/melody"]
28 |
--------------------------------------------------------------------------------
/rules/rules-available/nas.yml:
--------------------------------------------------------------------------------
1 | CVE-2020-9054 (ZyXEL NAS):
2 | layer: http
3 | meta:
4 | id: d7943e6c-4348-48b1-b2c5-0c553474a7ac
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "Checking or trying to exploit CVE-2020-9054"
11 | references:
12 | - "https://nvd.nist.gov/vuln/detail/CVE-2020-9054"
13 | match:
14 | http.uri:
15 | startswith:
16 | - "/adv,/cgi-bin/weblogin.cgi?username=admin"
17 | tags:
18 | cve: "cve-2020-9054"
19 | impact: "rce"
20 | device: "nas"
21 | vendor: "zyxel"
22 |
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Melody Documentation
2 | theme:
3 | name: material
4 | palette:
5 | primary: cyan
6 | nav:
7 | - Features: features.md
8 | - Requirements: requirements.md
9 | - Quickstart: quickstart.md
10 | - Installation: installation.md
11 | - Rules: rules.md
12 | - Layers: layers.md
13 | #- Use cases: use_cases.md
14 | - Links: links.md
15 | - Meloctl: meloctl.md
16 |
17 | markdown_extensions:
18 | - admonition
19 | - pymdownx.superfences
20 | - footnotes
21 | - toc:
22 | permalink: "#"
23 | plugins:
24 | - search
25 | - awesome-pages
26 |
27 | repo_url: https://github.com/ma111e/melody
28 | repo_name: ma111e/melody
29 |
--------------------------------------------------------------------------------
/rules/rules-available/rdp.yml:
--------------------------------------------------------------------------------
1 | RDP Login Attempt:
2 | layer: tcp
3 | meta:
4 | id: cbe12945-d9d1-4a9d-9138-e07c4d504eb7
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "RDP login attempt"
11 | references:
12 | - "https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/e78db616-689f-4b8a-8a99-525f7a433ee2"
13 | match:
14 | tcp.payload:
15 | startswith:
16 | - "Cookie: mstshash="
17 | offset: 11
18 | tags:
19 | proto: "rdp"
20 | action: "login"
21 | data: "username"
22 | techno: "remote_desktop"
23 |
--------------------------------------------------------------------------------
/internal/logdata/udp.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import "encoding/json"
4 |
5 | // UDPLogData is the struct describing the logged data for UDP packets
6 | type UDPLogData struct {
7 | Payload Payload `json:"payload"`
8 | Length uint16 `json:"length"`
9 | Checksum uint16 `json:"checksum"`
10 | }
11 |
12 | // UDPEventLog is the event log struct for UDP packets
13 | type UDPEventLog struct {
14 | UDP UDPLogData `json:"udp"`
15 | IP IPLogData `json:"ip"`
16 | BaseLogData
17 | }
18 |
19 | func (eventLog UDPEventLog) String() (string, error) {
20 | data, err := json.Marshal(eventLog)
21 | if err != nil {
22 | return "", err
23 | }
24 | return string(data), nil
25 | }
26 |
--------------------------------------------------------------------------------
/rules/rules-available/microsoft.yml:
--------------------------------------------------------------------------------
1 | CVE-2020-0618 (Microsoft SSRS - SQL Server Reporting Services):
2 | layer: http
3 | meta:
4 | id: 1663bfba-5033-4886-88ac-bcd45629200c
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "Scan for Microsoft SSRS endpoint, potentially trying to exploit it"
11 | references:
12 | - "https://nvd.nist.gov/vuln/detail/CVE-2020-0618"
13 | match:
14 | http.uri:
15 | startswith|nocase:
16 | - "/ReportServer"
17 | tags:
18 | cve: "cve-2020-0618"
19 | product: "ssrs"
20 | vendor: "microsoft"
21 | impact: "rce"
22 |
--------------------------------------------------------------------------------
/internal/logdata/payload.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import "encoding/base64"
4 |
5 | // Payload is the struct describing the logged data packets' payload when supported
6 | type Payload struct {
7 | Content string `json:"content"`
8 | Base64 string `json:"base64"`
9 | Truncated bool `json:"truncated"`
10 | }
11 |
12 | // NewPayloadLogData is used to create a new Payload struct
13 | func NewPayloadLogData(data []byte, maxLength uint64) Payload {
14 | var pl = Payload{}
15 |
16 | if uint64(len(data)) > maxLength {
17 | data = data[:maxLength]
18 | pl.Truncated = true
19 | }
20 | pl.Content = string(data)
21 | pl.Base64 = base64.StdEncoding.EncodeToString(data)
22 | return pl
23 | }
24 |
--------------------------------------------------------------------------------
/rules/rules-available/cms.yml:
--------------------------------------------------------------------------------
1 | CVE-2020-7961 Liferay Portal RCE:
2 | layer: http
3 | meta:
4 | id: 30adb6ba-a23e-48ba-bb12-8e91b54817c4
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "Checking or trying to exploit CVE-2020-7961"
11 | references:
12 | - "https://nvd.nist.gov/vuln/detail/CVE-2020-7961"
13 | match:
14 | http.uri:
15 | startswith:
16 | - "/api/jsonws/"
17 | http.method:
18 | is:
19 | - "POST"
20 | tags:
21 | cve: "cve-2020-7961"
22 | vendor: "liferay"
23 | product: "liferay_portal"
24 | impact: "rce"
25 | techno: "cms"
26 |
--------------------------------------------------------------------------------
/docs/docs/features.md:
--------------------------------------------------------------------------------
1 | Here are some key features of Melody :
2 |
3 | + Transparent capture
4 | + Write detection rules and tag specific packets to analyze them at scale
5 | + Mock vulnerable websites using the builtin HTTP/S server
6 | + Supports the main internet protocols over IPv4 and IPv6
7 | + Handles log rotation for you : Melody is designed to run forever on the smallest VPS
8 | + Minimal configuration required
9 | + Standalone mode : configure Melody using only the CLI
10 | + Easily scalable :
11 | + Statically compiled binary
12 | + Up-to-date Docker image
13 |
14 | Additional features on the roadmap include :
15 |
16 | + Dedicated helper program to create, test and manage rules
17 | + Centralized rules management
18 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "v*.*.*"
6 | jobs:
7 | goreleaser:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | with:
13 | fetch-depth: 0
14 | - name: Install build dependencies
15 | run: sudo apt update && sudo apt install libpcap-dev gcc-multilib
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: 1.16
20 | - name: Run GoReleaser
21 | uses: goreleaser/goreleaser-action@v2
22 | with:
23 | version: latest
24 | args: release --rm-dist
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/port_rules.yml:
--------------------------------------------------------------------------------
1 | ok_dst_ports:
2 | layer: tcp
3 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
4 | whitelist:
5 | ports:
6 | - 8080
7 | match:
8 | tcp.ack: 1335620269
9 |
10 | nok_dst_ports:
11 | layer: tcp
12 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
13 | whitelist:
14 | ports:
15 | - 0
16 | match:
17 | tcp.ack: 1335620269
18 |
19 | ok_dst_ports_range:
20 | layer: tcp
21 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
22 | whitelist:
23 | ports:
24 | - 8000 - 9000
25 | match:
26 | tcp.ack: 1335620269
27 |
28 | nok_dst_ports_range:
29 | layer: tcp
30 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
31 | whitelist:
32 | ports:
33 | - 0 - 10
34 | match:
35 | tcp.ack: 1335620269
36 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/ip_rules.yml:
--------------------------------------------------------------------------------
1 | ok_src_ips:
2 | layer: tcp
3 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
4 | whitelist:
5 | ips:
6 | - 127.0.0.1
7 | match:
8 | tcp.ack: 1335620269
9 |
10 | nok_src_ips:
11 | layer: tcp
12 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
13 | whitelist:
14 | ips:
15 | - 192.0.2.1
16 | match:
17 | tcp.ack: 1335620269
18 |
19 | ok_src_ips_range:
20 | layer: tcp
21 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
22 | whitelist:
23 | ips:
24 | - 127.0.0.0/24
25 | match:
26 | tcp.ack: 1335620269
27 |
28 | nok_src_ips_range:
29 | layer: tcp
30 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
31 | whitelist:
32 | ips:
33 | - 192.0.2.0/24
34 | match:
35 | tcp.ack: 1335620269
36 |
--------------------------------------------------------------------------------
/internal/events/event.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "github.com/google/gopacket/layers"
5 | "github.com/ma111e/melody/internal/loggable"
6 | )
7 |
8 | // Event is the interface implementing common methods to generated events
9 | type Event interface {
10 | //Match(rule rules.Rule) bool
11 | ToLog() EventLog
12 | GetIPHeader() *layers.IPv4
13 | GetICMPv6Header() *layers.ICMPv6
14 | GetICMPv4Header() *layers.ICMPv4
15 | GetUDPHeader() *layers.UDP
16 | GetTCPHeader() *layers.TCP
17 | GetHTTPData() HTTPEvent
18 |
19 | AddTags(tags map[string]string)
20 | AddAdditional(add map[string]string)
21 | loggable.Loggable
22 | }
23 |
24 | // EventLog is the interface implementing common methods to generated events' log data
25 | type EventLog interface {
26 | String() (string, error)
27 | }
28 |
--------------------------------------------------------------------------------
/internal/logdata/tcp.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | // TCPLogData is the struct describing the logged data for TCP packets
8 | type TCPLogData struct {
9 | Window uint16 `json:"window"`
10 | Seq uint32 `json:"seq"`
11 | Ack uint32 `json:"ack"`
12 | DataOffset uint8 `json:"data_offset"`
13 | Flags string `json:"flags"`
14 | Urgent uint16 `json:"urgent"`
15 | Payload Payload `json:"payload"`
16 | }
17 |
18 | // TCPEventLog is the event log struct for TCP packets
19 | type TCPEventLog struct {
20 | TCP TCPLogData `json:"tcp"`
21 | IP IPLogData `json:"ip"`
22 | BaseLogData
23 | }
24 |
25 | func (eventLog TCPEventLog) String() (string, error) {
26 | data, err := json.Marshal(eventLog)
27 | if err != nil {
28 | return "", err
29 | }
30 | return string(data), nil
31 | }
32 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | before:
3 | hooks:
4 | - go mod download
5 | builds:
6 | - id: melody
7 | main: ./cmd/melody
8 | binary: melody
9 | env:
10 | - CGO_ENABLED=1
11 | goos:
12 | - linux
13 | - windows
14 | goarch:
15 | - amd64
16 | - id: meloctl
17 | main: ./cmd/meloctl
18 | binary: meloctl
19 | env:
20 | - CGO_ENABLED=1
21 | goos:
22 | - linux
23 | - windows
24 | goarch:
25 | - amd64
26 | archives:
27 | -
28 | files:
29 | - rules/*
30 | - etc/*
31 | - var/*
32 | - filter.bpf
33 | - config.yml
34 | - docker-compose.yml
35 | - Dockerfile
36 | - Makefile
37 | wrap_in_directory: true
38 | checksum:
39 | name_template: 'checksums.txt'
40 | snapshot:
41 | version_template: "{{ .Tag }}-next"
42 | changelog:
43 | sort: asc
44 | filters:
45 | exclude:
46 | - '^docs:'
47 | - '^test:'
48 |
--------------------------------------------------------------------------------
/internal/clihelper/multistring.go:
--------------------------------------------------------------------------------
1 | package clihelper
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pborman/getopt/v2"
7 | )
8 |
9 | // MultiString is a getopt Option to allows passing multiple of the same option and get an array of values
10 | // Example : -o "a" -o "b" -o "c" -> [a, b, c]
11 | type MultiString []string
12 |
13 | // Set is the method called when the parser encounters a matching switch
14 | func (h *MultiString) Set(str string, opt getopt.Option) error {
15 | *h = append(*h, str)
16 | _ = opt
17 | return nil
18 | }
19 |
20 | // String is an helper to get the string representation of the option
21 | func (h *MultiString) String() string {
22 | return strings.Join(h.Array(), ", ")
23 | }
24 |
25 | // Array returns the values as an array
26 | func (h *MultiString) Array() []string {
27 | return *h
28 | }
29 |
30 | // ParseMultipleOptions returns the values parsed as an array
31 | func (h *MultiString) ParseMultipleOptions() []string {
32 | return h.Array()
33 | }
34 |
--------------------------------------------------------------------------------
/internal/logdata/icmpv6.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/google/gopacket/layers"
7 | )
8 |
9 | // ICMPv6LogData is the struct describing the logged data for ICMPv6 packets
10 | type ICMPv6LogData struct {
11 | TypeCode layers.ICMPv6TypeCode `json:"type_code"`
12 | Type uint8 `json:"type"`
13 | Code uint8 `json:"code"`
14 | TypeCodeName string `json:"type_code_name"`
15 | Checksum uint16 `json:"checksum"`
16 | Payload Payload `json:"payload"`
17 | }
18 |
19 | // ICMPv6EventLog is the event log struct for ICMPv6 packets
20 | type ICMPv6EventLog struct {
21 | ICMPv6 ICMPv6LogData `json:"icmpv6"`
22 | IP IPv6LogData `json:"ip"`
23 | BaseLogData
24 | }
25 |
26 | func (eventLog ICMPv6EventLog) String() (string, error) {
27 | data, err := json.Marshal(eventLog)
28 | if err != nil {
29 | return "", err
30 | }
31 | return string(data), nil
32 | }
33 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ma111e/melody
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
7 | github.com/fatih/structtag v1.2.0
8 | github.com/google/gopacket v1.1.19
9 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
10 | github.com/google/uuid v1.2.0
11 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
12 | github.com/k0kubun/pp v3.0.1+incompatible
13 | github.com/kr/text v0.2.0 // indirect
14 | github.com/manifoldco/promptui v0.8.0
15 | github.com/natefinch/lumberjack v2.0.0+incompatible
16 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
17 | github.com/pborman/getopt/v2 v2.1.0
18 | github.com/rs/xid v1.2.1
19 | github.com/spf13/cobra v1.1.3
20 | golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
21 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect
22 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
23 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
24 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
25 | )
26 |
--------------------------------------------------------------------------------
/internal/logdata/icmpv4.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/google/gopacket/layers"
7 | )
8 |
9 | // ICMPv4LogData is the struct describing the logged data for ICMPv4 packets
10 | type ICMPv4LogData struct {
11 | TypeCode layers.ICMPv4TypeCode `json:"type_code"`
12 | Type uint8 `json:"type"`
13 | Code uint8 `json:"code"`
14 | TypeCodeName string `json:"type_code_name"`
15 | Checksum uint16 `json:"checksum"`
16 | ID uint16 `json:"id"`
17 | Seq uint16 `json:"seq"`
18 | Payload Payload `json:"payload"`
19 | }
20 |
21 | // ICMPv4EventLog is the event log struct for ICMPv4 packets
22 | type ICMPv4EventLog struct {
23 | ICMPv4 ICMPv4LogData `json:"icmpv4"`
24 | IP IPv4LogData `json:"ip"`
25 | BaseLogData
26 | }
27 |
28 | func (eventLog ICMPv4EventLog) String() (string, error) {
29 | data, err := json.Marshal(eventLog)
30 | if err != nil {
31 | return "", err
32 | }
33 | return string(data), nil
34 | }
35 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/icmpv4_rules.yml:
--------------------------------------------------------------------------------
1 | ok_icmpv4_typecode:
2 | layer: icmpv4
3 | id: 5ab4a5de-0eff-4045-a50d-e3d44159013b
4 | match:
5 | icmpv4.typecode: 2048
6 |
7 | nok_icmpv4_typecode:
8 | layer: icmpv4
9 | id: 817407fc-5dec-42ca-8e42-1198cd51afdc
10 | match:
11 | icmpv4.typecode: 0
12 |
13 | ok_checksum:
14 | layer: icmpv4
15 | id: b845d07e-c447-4b4c-aad7-1be33bc4fa4a
16 | match:
17 | icmpv4.checksum: 0x0416
18 |
19 | nok_checksum:
20 | layer: icmpv4
21 | id: ca48fc15-c273-43f8-8487-04507f4f4813
22 | match:
23 | icmpv4.checksum: 0
24 |
25 | ok_icmpv4_code:
26 | layer: icmpv4
27 | id: b845d07e-c447-4b4c-aad7-1be33bc4fa4a
28 | match:
29 | icmpv4.code: 0
30 |
31 | nok_icmpv4_code:
32 | layer: icmpv4
33 | id: ca48fc15-c273-43f8-8487-04507f4f4813
34 | match:
35 | icmpv4.code: 1
36 |
37 | ok_icmpv4_type:
38 | layer: icmpv4
39 | id: b845d07e-c447-4b4c-aad7-1be33bc4fa4a
40 | match:
41 | icmpv4.type: 0x8
42 |
43 | nok_icmpv4_type:
44 | layer: icmpv4
45 | id: ca48fc15-c273-43f8-8487-04507f4f4813
46 | match:
47 | icmpv4.type: 0
48 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/icmpv6_rules.yml:
--------------------------------------------------------------------------------
1 | ok_icmpv6_typecode:
2 | layer: icmpv6
3 | id: 5ab6a5de-0eff-4065-a50d-e3d44159013b
4 | match:
5 | icmpv6.typecode: 32768
6 |
7 | nok_icmpv6_typecode:
8 | layer: icmpv6
9 | id: 817407fc-5dec-42ca-8e62-1198cd51afdc
10 | match:
11 | icmpv6.typecode: 0
12 |
13 | ok_checksum:
14 | layer: icmpv6
15 | id: b845d07e-c447-4b4c-aad7-1be33bc6fa6a
16 | match:
17 | icmpv6.checksum: 0x275b
18 |
19 | nok_checksum:
20 | layer: icmpv6
21 | id: ca68fc15-c273-43f8-8687-04507f4f4813
22 | match:
23 | icmpv6.checksum: 0
24 |
25 | ok_icmpv6_code:
26 | layer: icmpv6
27 | id: b845d07e-c447-4b4c-aad7-1be33bc6fa6a
28 | match:
29 | icmpv6.code: 0
30 |
31 | nok_icmpv6_code:
32 | layer: icmpv6
33 | id: ca68fc15-c273-43f8-8687-04507f4f4813
34 | match:
35 | icmpv6.code: 1
36 |
37 | ok_icmpv6_type:
38 | layer: icmpv6
39 | id: b845d07e-c447-4b4c-aad7-1be33bc6fa6a
40 | match:
41 | icmpv6.type: 0x80
42 |
43 | nok_icmpv6_type:
44 | layer: icmpv6
45 | id: ca68fc15-c273-43f8-8687-04507f4f4813
46 | match:
47 | icmpv6.type: 0
48 |
--------------------------------------------------------------------------------
/internal/config/tagparser.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/ma111e/melody/internal/tagparser"
7 | )
8 |
9 | // LoadYAMLTagsOf loads the yaml tags of a struct
10 | func LoadYAMLTagsOf(what interface{}) ([]string, error) {
11 | var tags []string
12 |
13 | for i := 0; i < reflect.TypeOf(what).NumField(); i++ {
14 | ruleTag := reflect.TypeOf(what).Field(i).Tag
15 |
16 | _, exists := ruleTag.Lookup("yaml")
17 | if !exists {
18 | continue
19 | }
20 |
21 | tagValue, err := tagparser.ParseYamlTagValue(ruleTag)
22 | if err != nil {
23 | return tags, err
24 | }
25 |
26 | tags = append(tags, tagValue)
27 | }
28 |
29 | return tags, nil
30 | }
31 |
32 | // LoadValidConfigKeysMap returns a map of the json keys present in the Config struct
33 | func LoadValidConfigKeysMap() map[string]interface{} {
34 | configKeysMap := make(map[string]interface{})
35 | tags, err := LoadYAMLTagsOf(Config{})
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | for _, tag := range tags {
41 | configKeysMap[tag] = new(interface{})
42 | }
43 |
44 | return configKeysMap
45 | }
46 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/udp_rules.yml:
--------------------------------------------------------------------------------
1 | ok_checksum:
2 | layer: udp
3 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
4 | match:
5 | udp.checksum: 0xfe37
6 |
7 | nok_checksum:
8 | layer: udp
9 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
10 | match:
11 | udp.checksum: 0
12 |
13 | ok_dsize:
14 | layer: udp
15 | id: dc65fcd2-90bc-4b00-894b-7fe936be0298
16 | match:
17 | udp.dsize: 28
18 |
19 | nok_dsize:
20 | layer: udp
21 | id: 67c669bf-585d-4935-8c0c-4d3d18d996fc
22 | match:
23 | udp.dsize: 0
24 |
25 | ok_length:
26 | layer: udp
27 | id: dc65fcd2-90bc-4b00-894b-7fe936be0298
28 | match:
29 | udp.length: 36
30 |
31 | nok_length:
32 | layer: udp
33 | id: 67c669bf-585d-4935-8c0c-4d3d18d996fc
34 | match:
35 | udp.length: 0
36 |
37 | ok_payload:
38 | layer: udp
39 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
40 | match:
41 | udp.payload:
42 | is:
43 | - "after all, we're all alike.\n"
44 |
45 | nok_payload:
46 | layer: udp
47 | id: ae6de878-80fc-4052-bc64-a92beb425248
48 | match:
49 | udp.payload:
50 | is:
51 | - "nonexistent"
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 ma111e
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | Welcome to the ~**Melodoc**~.
2 |
3 | Melody is an internet sensor built for threat intelligence. This tool have multiple use cases :
4 |
5 | + Build historic data to extract trends and patterns
6 | + Keep an eye on specific threats
7 | + Monitor emerging threats exploitation
8 | + Index malicious activity by detecting exploitation attempts and targeted scanners
9 | + Log every contact your application receives from the internet to find potentially malicious activity
10 |
11 | Deploying it can be as easy as pulling the latest compiled binary or the official Docker image.
12 |
13 | Add your favorite rules, some configuration tweaks, a BPF to clean the noise a bit and then forget it[^1] and let the internet symphony flow to you.
14 |
15 | You can tweak the options either with a file or directly by passing options trough the CLI, allowing Melody to act as a standalone application.
16 |
17 | Melody will also handle log rotation for you. It has been designed to be able to run forever on the smallest VPS while handling millions of packets a day.
18 |
19 | [^1]: You should either setup an automated patching process or come back often to apply security patches on the host though
20 |
--------------------------------------------------------------------------------
/internal/logdata/http.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import "encoding/json"
4 |
5 | // HTTPLogData is the struct describing the logged data for reassembled HTTP packets
6 | type HTTPLogData struct {
7 | Verb string `json:"verb"`
8 | Proto string `json:"proto"`
9 | RequestURI string `json:"uri"`
10 | SourcePort uint16 `json:"src_port"`
11 | DestHost string `json:"dst_host"`
12 | UserAgent string `json:"user_agent"`
13 | Headers map[string]string `json:"headers"`
14 | HeadersKeys []string `json:"headers_keys"`
15 | HeadersValues []string `json:"headers_values"`
16 | Errors []string `json:"errors"`
17 | Body Payload `json:"body"`
18 | IsTLS bool `json:"is_tls"`
19 | }
20 |
21 | // HTTPEventLog is the event log struct for reassembled HTTP packets
22 | type HTTPEventLog struct {
23 | HTTP HTTPLogData `json:"http"`
24 | IP IPLogData `json:"ip"`
25 | BaseLogData
26 | }
27 |
28 | func (eventLog HTTPEventLog) String() (string, error) {
29 | data, err := json.Marshal(eventLog)
30 | if err != nil {
31 | return "", err
32 | }
33 | return string(data), nil
34 | }
35 |
--------------------------------------------------------------------------------
/internal/logdata/ipv6.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import (
4 | "github.com/google/gopacket/layers"
5 | "github.com/ma111e/melody/internal/events/helpers"
6 | )
7 |
8 | // IPv6LogData is the struct describing the logged data for IPv6 header
9 | type IPv6LogData struct {
10 | Version uint8 `json:"version"`
11 | Length uint16 `json:"length"`
12 | NextHeader layers.IPProtocol `json:"next_header"`
13 | NextHeaderName string `json:"next_header_name"`
14 | TrafficClass uint8 `json:"traffic_class"`
15 | FlowLabel uint32 `json:"flow_label"`
16 | HopLimit uint8 `json:"hop_limit"`
17 | IPLogData `json:"-"`
18 | }
19 |
20 | // NewIPv6LogData is used to create a new IPv6LogData struct
21 | func NewIPv6LogData(ipv6Layer helpers.IPv6Layer) IPv6LogData {
22 | return IPv6LogData{
23 | Version: ipv6Layer.Header.Version,
24 | Length: ipv6Layer.Header.Length,
25 | NextHeader: ipv6Layer.Header.NextHeader,
26 | NextHeaderName: ipv6Layer.Header.NextHeader.String(),
27 | TrafficClass: ipv6Layer.Header.TrafficClass,
28 | FlowLabel: ipv6Layer.Header.FlowLabel,
29 | HopLimit: ipv6Layer.Header.HopLimit,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/docs/requirements.md:
--------------------------------------------------------------------------------
1 | ## Go version
2 | Minimum supported Go version is 1.11, but we recommend using Go 1.13+ since that's the lowest version we're using for testing.
3 |
4 | ## libpcap
5 |
6 | You'll need the `libpcap` C headers to build the program.
7 |
8 | Install it with :
9 |
10 | ```
11 | sudo apt update
12 | sudo apt install libpcap-dev
13 | ```
14 |
15 | !!! Note
16 | You won't need them if you're using Docker or a pre-compiled release binary.
17 |
18 | ## HTTPS dummy server
19 | You'll need TLS certificates in order to use the built-in dummy HTTPS server.
20 |
21 | Use one of these commands to generate them for you :
22 |
23 | ```
24 | make certs
25 | ```
26 |
27 | or
28 |
29 | ```
30 | mkdir -p var/https/certs
31 | openssl req -x509 -subj "/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=localhost" -newkey rsa:4096 -keyout var/https/certs/key.pem -out var/https/certs/cert.pem -days 3650 -nodes
32 | ```
33 |
34 | !!! Warning
35 | Using these commands will overwrite any `cert.pem` or `key.pem` file already present in `$melody/var/https/certs/`
36 |
37 | !!! Tip
38 | You can also use your own by putting the `key.pem` and `cert.pem` in `$melody/var/https/certs`. **Keep in mind that it might be used by attackers to fingerprint or gain information on your infrastructure.**
39 |
--------------------------------------------------------------------------------
/internal/router/fs.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "path"
7 | "path/filepath"
8 | "strings"
9 | )
10 |
11 | // https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings#using-a-custom-filesystem
12 | type neuteredFileSystem struct {
13 | fs http.FileSystem
14 | }
15 |
16 | func melodyFs(root http.FileSystem, notFoundCode int) http.Handler {
17 | fs := http.FileServer(
18 | neuteredFileSystem{
19 | root,
20 | })
21 |
22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 | //make sure the url path starts with /
24 | upath := r.URL.Path
25 | if !strings.HasPrefix(upath, "/") {
26 | upath = "/" + upath
27 | r.URL.Path = upath
28 | }
29 | upath = path.Clean(upath)
30 |
31 | // attempt to open the file via the http.FileSystem
32 | f, err := root.Open(upath)
33 | if err != nil {
34 | if os.IsNotExist(err) {
35 | w.WriteHeader(notFoundCode)
36 | _, _ = w.Write([]byte{})
37 | return
38 | }
39 | }
40 |
41 | s, _ := f.Stat()
42 | if s.IsDir() {
43 | index := filepath.Join(upath, "index.html")
44 | if _, err := root.Open(index); err != nil {
45 | _ = f.Close()
46 | if os.IsNotExist(err) {
47 | w.WriteHeader(notFoundCode)
48 | _, _ = w.Write([]byte{})
49 | return
50 | }
51 | }
52 | }
53 |
54 | fs.ServeHTTP(w, r)
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/internal/sessions/sessions.go:
--------------------------------------------------------------------------------
1 | package sessions
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/rs/xid"
7 | )
8 |
9 | // Session abstracts a session made of a last seen record and an uid
10 | type Session struct {
11 | lastSeen time.Time
12 | uid string
13 | }
14 |
15 | // sessionMap abstracts a hash table of multiple Session sorted by their flow data
16 | type sessionMap map[string]*Session
17 |
18 | var (
19 | // SessionMap is the global sessions hash table
20 | SessionMap = make(sessionMap)
21 | )
22 |
23 | func (m sessionMap) GetUID(flow string) string {
24 | if session, ok := m[flow]; ok {
25 | return session.uid
26 | }
27 |
28 | return m.add(flow)
29 | }
30 |
31 | func (m *sessionMap) add(flow string) string {
32 | //var ts = strconv.FormatInt(time.Now().UnixNano(), 10)
33 | var ts = xid.New().String()
34 |
35 | (*m)[flow] = &Session{
36 | uid: ts,
37 | lastSeen: time.Now(),
38 | }
39 | return ts
40 | }
41 |
42 | // FlushOlderThan cleans the session mapping of sessions not seen since the given deadline
43 | func (m *sessionMap) FlushOlderThan(deadline time.Time) {
44 | for flow, session := range *m {
45 | if session.lastSeen.Before(deadline) {
46 | delete(*m, flow)
47 | }
48 | }
49 | }
50 |
51 | // FlushAll removes all sessions from the session mapping
52 | func (m *sessionMap) FlushAll() {
53 | for flow := range *m {
54 | delete(*m, flow)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/internal/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/ma111e/melody/internal/events"
9 |
10 | "github.com/ma111e/melody/internal/logging"
11 |
12 | "github.com/ma111e/melody/internal/config"
13 | )
14 |
15 | // StartHTTP starts the dummy HTTP server
16 | func StartHTTP(quitErrChan chan error) {
17 | r := http.NewServeMux()
18 | r.Handle("/",
19 | headersHandler(
20 | melodyFs(http.Dir(config.Cfg.ServerHTTPDir), config.Cfg.ServerHTTPMissingResponseStatus),
21 | config.Cfg.ServerHTTPHeaders))
22 |
23 | logging.Std.Println("Started HTTP server on port", config.Cfg.ServerHTTPPort)
24 | quitErrChan <- http.ListenAndServe(fmt.Sprintf(":%d", config.Cfg.ServerHTTPPort), r)
25 | }
26 |
27 | // StartHTTPS starts the dummy HTTPS server
28 | func StartHTTPS(quitErrChan chan error, eventChan chan events.Event) {
29 | r := http.NewServeMux()
30 | r.Handle("/",
31 | httpsLogger(
32 | headersHandler(
33 | melodyFs(http.Dir(config.Cfg.ServerHTTPSDir), config.Cfg.ServerHTTPSMissingResponseStatus),
34 | config.Cfg.ServerHTTPSHeaders), eventChan))
35 |
36 | srv := &http.Server{
37 | Addr: fmt.Sprintf(":%d", config.Cfg.ServerHTTPSPort),
38 | Handler: r,
39 | TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
40 | }
41 |
42 | logging.Std.Println("Started HTTPS server on port", config.Cfg.ServerHTTPSPort)
43 | quitErrChan <- srv.ListenAndServeTLS(config.Cfg.ServerHTTPSCert, config.Cfg.ServerHTTPSKey)
44 | }
45 |
--------------------------------------------------------------------------------
/internal/assembler/stream.go:
--------------------------------------------------------------------------------
1 | package assembler
2 |
3 | import (
4 | "bufio"
5 | "github.com/google/gopacket"
6 | "github.com/google/gopacket/tcpassembly"
7 | "github.com/google/gopacket/tcpassembly/tcpreader"
8 | "github.com/ma111e/melody/internal/engine"
9 | "github.com/ma111e/melody/internal/events"
10 | "io"
11 | "net/http"
12 | )
13 |
14 | // HTTPStreamFactory implements tcpassembly.StreamFactory
15 | type HTTPStreamFactory struct{}
16 |
17 | // HTTPStream will handle the actual decoding of http requests.
18 | type HTTPStream struct {
19 | net, transport gopacket.Flow
20 | r tcpreader.ReaderStream
21 | }
22 |
23 | // New creates a new HTTPStreamFactory from the given flow data
24 | func (h *HTTPStreamFactory) New(net, transport gopacket.Flow) tcpassembly.Stream {
25 | hstream := &HTTPStream{
26 | net: net,
27 | transport: transport,
28 | r: tcpreader.NewReaderStream(),
29 | }
30 | go hstream.run() // Important... we must guarantee that data from the reader stream is read.
31 |
32 | // ReaderStream implements tcpassembly.Stream, so we can return a pointer to it.
33 | return &hstream.r
34 | }
35 |
36 | func (h *HTTPStream) run() {
37 | buf := bufio.NewReader(&h.r)
38 | for {
39 | req, err := http.ReadRequest(buf)
40 | if err == io.EOF {
41 | // We must read until we see an EOF... very important!
42 | return
43 | } else if err != nil {
44 |
45 | } else {
46 | ev, _ := events.NewHTTPEvent(req, h.net, h.transport)
47 | engine.EventChan <- ev
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/meloctl/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 |
7 | "gopkg.in/yaml.v3"
8 |
9 | "github.com/ma111e/melody/internal/config"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var (
15 | configCmd = &cobra.Command{
16 | Use: "config",
17 | Short: "Interact with a Melody config file",
18 | Long: `This subcommand is used to interact with a Melody config file`,
19 | }
20 | checkConfigCmd = &cobra.Command{
21 | Use: "check",
22 | Args: cobra.ExactArgs(1),
23 | Short: "Validate a Melody config file",
24 | Long: `This subcommand is used to validate a Melody config file`,
25 | Run: checkConfig,
26 | }
27 |
28 | validConfigKeysMap map[string]interface{} = config.LoadValidConfigKeysMap()
29 | )
30 |
31 | func init() {
32 | RootCmd.AddCommand(configCmd)
33 | configCmd.AddCommand(checkConfigCmd)
34 | }
35 |
36 | func checkConfig(_ *cobra.Command, args []string) {
37 | var err error
38 | configPath := args[0]
39 |
40 | cfg := make(map[string]interface{})
41 |
42 | cfgData, err := ioutil.ReadFile(configPath)
43 | if err != nil {
44 | fmt.Printf("❌ [%s]: %s\n", configPath, err)
45 | return
46 | }
47 |
48 | if err := yaml.Unmarshal(cfgData, &cfg); err != nil {
49 | fmt.Printf("❌ [%s]: %s\n", configPath, err)
50 | return
51 | }
52 |
53 | for key := range cfg {
54 | if _, ok := validConfigKeysMap[key]; !ok {
55 | fmt.Printf("❌ [%s]: unknown property '%s'\n", configPath, key)
56 | return
57 | }
58 | }
59 |
60 | fmt.Printf("✅ [%s]: OK\n", configPath)
61 | }
62 |
--------------------------------------------------------------------------------
/internal/router/middlewares.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "net/http"
5 | "path/filepath"
6 |
7 | "github.com/ma111e/melody/internal/config"
8 |
9 | "github.com/ma111e/melody/internal/logging"
10 |
11 | "github.com/ma111e/melody/internal/events"
12 | )
13 |
14 | func (nfs neuteredFileSystem) Open(path string) (http.File, error) {
15 | f, err := nfs.fs.Open(path)
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | s, _ := f.Stat()
21 | if s.IsDir() {
22 | index := filepath.Join(path, "index.html")
23 | if _, err := nfs.fs.Open(index); err != nil {
24 | closeErr := f.Close()
25 | if closeErr != nil {
26 | return nil, closeErr
27 | }
28 |
29 | return nil, err
30 | }
31 | }
32 |
33 | return f, nil
34 | }
35 |
36 | func headersHandler(h http.Handler, headers map[string]string) http.Handler {
37 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38 | for header, val := range headers {
39 | w.Header().Set(header, val)
40 | }
41 | h.ServeHTTP(w, r) // pass request
42 | })
43 | }
44 |
45 | func httpsLogger(h http.Handler, eventChan chan events.Event) http.Handler {
46 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47 | if _, ok := config.Cfg.DiscardProto4[config.HTTPSKind]; ok {
48 | h.ServeHTTP(w, r) // pass request
49 | return
50 | } else if _, ok := config.Cfg.DiscardProto6[config.HTTPSKind]; ok {
51 | h.ServeHTTP(w, r) // pass request
52 | return
53 | }
54 |
55 | ev, err := events.NewHTTPEventFromRequest(r)
56 | if err != nil {
57 | logging.Errors.Println(err)
58 | return
59 | }
60 | eventChan <- ev
61 |
62 | h.ServeHTTP(w, r) // pass request
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - "**"
6 | pull_request:
7 | branches:
8 | - "**"
9 | jobs:
10 | tests:
11 | name: Unit and integrations tests
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | go: ["1.13", "1.14", "1.15", "1.16"]
17 | steps:
18 | - name: Install libpcap dependencies
19 | run: sudo apt update && sudo apt install libpcap-dev
20 | - uses: actions/setup-go@v2
21 | with:
22 | go-version: ${{ matrix.go }}
23 | - uses: actions/checkout@v2
24 | - name: Run tests
25 | run: |
26 | go test -v -p 1 -race -coverprofile=covprofile -covermode=atomic ./...
27 | - if: ${{ matrix.go == 1.15 }}
28 | name: Update coverage
29 | uses: shogo82148/actions-goveralls@v1.4.2
30 | with:
31 | path-to-profile: covprofile
32 |
33 | lint:
34 | name: Lint
35 | runs-on: ubuntu-latest
36 | steps:
37 | - name: Install libpcap dependencies
38 | run: sudo apt update && sudo apt install libpcap-dev
39 | - uses: actions/checkout@v2
40 | - name: Run lint
41 | uses: golangci/golangci-lint-action@v2
42 | with:
43 | version: v1.32
44 | args: --timeout 5m
45 |
46 | spellcheck:
47 | name: Spellcheck docs
48 | runs-on: ubuntu-latest
49 | steps:
50 | - uses: actions/checkout@v2
51 | - uses: actions/setup-go@v2
52 | with:
53 | go-version: 1.15
54 | - name: Run spellcheck
55 | run: |
56 | go get github.com/client9/misspell/cmd/misspell
57 | misspell -error docs/docs
58 |
--------------------------------------------------------------------------------
/internal/logdata/base.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ma111e/melody/internal/loggable"
7 | )
8 |
9 | // IPLogData is the interface used by packet structs supporting an IP layer
10 | type IPLogData interface{}
11 |
12 | // BaseLogData is used as the base packet log and contains common data, such as the timestamp
13 | type BaseLogData struct {
14 | Timestamp string `json:"timestamp"`
15 | Session string `json:"session"`
16 | Type string `json:"type"`
17 | SourceIP string `json:"src_ip"`
18 | DestPort uint16 `json:"dst_port"`
19 | Tags map[string][]string `json:"matches"`
20 | InlineTags []string `json:"inline_matches"`
21 | Additional map[string]string `json:"embedded"`
22 | }
23 |
24 | // Init takes the common BaseEvent attributes to setup the BaseLogData struct
25 | func (l *BaseLogData) Init(ev loggable.Loggable) {
26 | l.Type = ev.GetKind()
27 | l.SourceIP = ev.GetSourceIP()
28 | l.DestPort = ev.GetDestPort()
29 | l.Session = ev.GetSession()
30 | l.InlineTags = []string{}
31 |
32 | if len(ev.GetTags()) == 0 {
33 | l.Tags = make(map[string][]string)
34 | } else {
35 | l.Tags = ev.GetTags()
36 | l.InlineTags = makeInlineArray(ev.GetTags())
37 | }
38 | }
39 |
40 | // makeInlineArray converts a Tags map to an array of its values with the keys and values merged with a '.'
41 | func makeInlineArray(tags map[string][]string) []string {
42 | var inlineTags []string
43 |
44 | for key, values := range tags {
45 | for _, val := range values {
46 | inlineTags = append(inlineTags, fmt.Sprintf("%s.%s", key, val))
47 | }
48 | }
49 |
50 | return inlineTags
51 | }
52 |
--------------------------------------------------------------------------------
/internal/logdata/ipv4.go:
--------------------------------------------------------------------------------
1 | package logdata
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/google/gopacket/layers"
7 | "github.com/ma111e/melody/internal/events/helpers"
8 | )
9 |
10 | // IPv4LogData is the struct describing the logged data for IPv4 header
11 | type IPv4LogData struct {
12 | Version uint8 `json:"version"`
13 | IHL uint8 `json:"ihl"`
14 | TOS uint8 `json:"tos"`
15 | Length uint16 `json:"length"`
16 | ID uint16 `json:"id"`
17 | Fragbits string `json:"fragbits"`
18 | FragOffset uint16 `json:"frag_offset"`
19 | TTL uint8 `json:"ttl"`
20 | Protocol layers.IPProtocol `json:"protocol"`
21 | IPLogData `json:"-"`
22 | }
23 |
24 | // NewIPv4LogData is used to create a new IPv4LogData struct
25 | func NewIPv4LogData(ipv4Layer helpers.IPv4Layer) IPv4LogData {
26 | var ipFlagsStr []string
27 |
28 | if ipv4Layer.Header.Flags&layers.IPv4EvilBit != 0 {
29 | ipFlagsStr = append(ipFlagsStr, "EV")
30 | }
31 | if ipv4Layer.Header.Flags&layers.IPv4DontFragment != 0 {
32 | ipFlagsStr = append(ipFlagsStr, "DF")
33 | }
34 | if ipv4Layer.Header.Flags&layers.IPv4MoreFragments != 0 {
35 | ipFlagsStr = append(ipFlagsStr, "MF")
36 | }
37 |
38 | data := IPv4LogData{
39 | Version: ipv4Layer.Header.Version,
40 | IHL: ipv4Layer.Header.IHL,
41 | TOS: ipv4Layer.Header.TOS,
42 | Length: ipv4Layer.Header.Length,
43 | ID: ipv4Layer.Header.Id,
44 | FragOffset: ipv4Layer.Header.FragOffset,
45 | TTL: ipv4Layer.Header.TTL,
46 | Protocol: ipv4Layer.Header.Protocol,
47 | Fragbits: strings.Join(ipFlagsStr, ""),
48 | }
49 |
50 | return data
51 | }
52 |
--------------------------------------------------------------------------------
/internal/engine/engine.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "github.com/ma111e/melody/internal/config"
5 | "github.com/ma111e/melody/internal/events"
6 | "github.com/ma111e/melody/internal/logging"
7 | "github.com/ma111e/melody/internal/router"
8 | "github.com/ma111e/melody/internal/rules"
9 | )
10 |
11 | var (
12 | // EventChan is the channel used to receive event to qualify
13 | EventChan = make(chan events.Event)
14 | )
15 |
16 | // Start starts the matching and tagging engine
17 | func Start(quitErrChan chan error, shutdownChan chan bool, engineStoppedChan chan bool) {
18 | go startEventQualifier(quitErrChan, shutdownChan, engineStoppedChan)
19 |
20 | if config.Cfg.ServerHTTPEnable {
21 | logging.Std.Println("Starting HTTP server")
22 | go router.StartHTTP(quitErrChan)
23 | }
24 |
25 | if config.Cfg.ServerHTTPSEnable {
26 | logging.Std.Println("Starting HTTPS server")
27 | go router.StartHTTPS(quitErrChan, EventChan)
28 | }
29 | }
30 |
31 | func startEventQualifier(quitErrChan chan error, shutdownChan chan bool, engineStoppedChan chan bool) {
32 | defer func() {
33 | close(engineStoppedChan)
34 | }()
35 |
36 | for {
37 | select {
38 | case <-shutdownChan:
39 | return
40 |
41 | case <-quitErrChan:
42 | return
43 |
44 | case ev := <-EventChan:
45 | var matches []rules.Rule
46 |
47 | for _, ruleset := range rules.GlobalRules[ev.GetKind()] {
48 | for _, rule := range ruleset {
49 | if rule.Match(ev) {
50 | matches = append(matches, rule)
51 | }
52 | }
53 | }
54 |
55 | if len(matches) > 0 {
56 | for _, match := range matches {
57 | ev.AddTags(match.Tags)
58 | ev.AddAdditional(match.Additional)
59 | }
60 | }
61 |
62 | logging.LogChan <- ev
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/internal/filters/utils.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | // From https://github.com/apparentlymart/go-cidr/blob/master/cidr/cidr.go
4 | import (
5 | "fmt"
6 | "math/big"
7 | "net"
8 | )
9 |
10 | func addressRange(network *net.IPNet) (net.IP, net.IP, error) {
11 | // the first IP is easy
12 | firstIP := network.IP
13 |
14 | // the last IP is the network address OR NOT the mask address
15 | prefixLen, bits := network.Mask.Size()
16 | if prefixLen == bits {
17 | // Easy!
18 | // But make sure that our two slices are distinct, since they
19 | // would be in all other cases.
20 | lastIP := make([]byte, len(firstIP))
21 | copy(lastIP, firstIP)
22 | return firstIP, lastIP, nil
23 | }
24 |
25 | firstIPInt, bits, err := ipToInt(firstIP)
26 | if err != nil {
27 | return net.IP{}, net.IP{}, err
28 | }
29 |
30 | hostLen := uint(bits) - uint(prefixLen)
31 | lastIPInt := big.NewInt(1)
32 | lastIPInt.Lsh(lastIPInt, hostLen)
33 | lastIPInt.Sub(lastIPInt, big.NewInt(1))
34 | lastIPInt.Or(lastIPInt, firstIPInt)
35 |
36 | return firstIP, intToIP(lastIPInt, bits), nil
37 | }
38 |
39 | func ipToInt(ip net.IP) (*big.Int, int, error) {
40 | val := &big.Int{}
41 | val.SetBytes(ip)
42 | if len(ip) == net.IPv4len {
43 | return val, 32, nil
44 | } else if len(ip) == net.IPv6len {
45 | return val, 128, nil
46 | }
47 |
48 | return nil, 0, fmt.Errorf("unsupported address length %d", len(ip))
49 | }
50 |
51 | func intToIP(ipInt *big.Int, bits int) net.IP {
52 | ipBytes := ipInt.Bytes()
53 | ret := make([]byte, bits/8)
54 | // Pack our IP bytes into the end of the return array,
55 | // since big.Int.Bytes() removes front zero padding.
56 | for i := 1; i <= len(ipBytes); i++ {
57 | ret[len(ret)-i] = ipBytes[len(ipBytes)-i]
58 | }
59 | return ret
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/meloctl/get.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ma111e/melody/internal/config"
6 | "github.com/ma111e/melody/internal/tagparser"
7 | "reflect"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | var (
13 | getCmd = &cobra.Command{
14 | Args: cobra.ExactArgs(1),
15 | Use: "get",
16 | Short: "Get a Meloctl config value by name",
17 | Long: `This subcommand is used to get a Meloctl config value by name`,
18 | Run: getConfigKey,
19 | }
20 | )
21 |
22 | func init() {
23 | RootCmd.AddCommand(getCmd)
24 | }
25 |
26 | func getConfigKey(_ *cobra.Command, args []string) {
27 | var found bool
28 | key := args[0]
29 |
30 | validKeys, _ := config.LoadYAMLTagsOf(MeloctlConfig{})
31 | for _, valid := range validKeys {
32 | if key == valid {
33 | found = true
34 | break
35 | }
36 | }
37 |
38 | if !found {
39 | fmt.Printf("❌ [%s] Unknown key\n", key)
40 | return
41 | }
42 |
43 | val, err := getValueByYAMLTagName(meloctlConf, key)
44 | if err != nil {
45 | fmt.Printf("❌ Error while fetching value : %s\n", err)
46 | return
47 | }
48 |
49 | fmt.Printf("%s => %s \n", key, val)
50 | }
51 |
52 | func getValueByYAMLTagName(what interface{}, key string) (string, error) {
53 | var val string
54 |
55 | for i := 0; i < reflect.TypeOf(what).Elem().NumField(); i++ {
56 | tag := reflect.TypeOf(what).Elem().Field(i).Tag
57 |
58 | _, exists := tag.Lookup("yaml")
59 | if !exists {
60 | continue
61 | }
62 |
63 | tagValue, err := tagparser.ParseYamlTagValue(tag)
64 | if err != nil {
65 | return val, err
66 | }
67 |
68 | if key == tagValue {
69 | val = reflect.ValueOf(what).Elem().FieldByName(reflect.TypeOf(what).Elem().Field(i).Name).String()
70 | break
71 | }
72 | }
73 |
74 | return val, nil
75 | }
76 |
--------------------------------------------------------------------------------
/cmd/meloctl/init.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ma111e/melody/internal/fileutils"
6 | "github.com/ma111e/melody/internal/meloctl/prompt"
7 | "io/ioutil"
8 | "os"
9 |
10 | "gopkg.in/yaml.v3"
11 |
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | // MeloctlConfig represents the meloctl config file
16 | type MeloctlConfig struct {
17 | MelodyHomeDir string `yaml:"melody.home" pretty:"Melody home directory"`
18 | //MelodyRulesSource string `yaml:"melody.rules.sources" pretty:"Melody rules sources"`
19 | }
20 |
21 | var (
22 | initCmd = &cobra.Command{
23 | Use: "init",
24 | Short: "Create Meloctl config",
25 | Long: `This subcommand is used to setup Meloctl`,
26 | Run: initConfig,
27 | }
28 |
29 | rawMeloconf = map[string]string{}
30 | )
31 |
32 | func init() {
33 | RootCmd.AddCommand(initCmd)
34 | }
35 |
36 | func initConfig(_ *cobra.Command, args []string) {
37 | _ = os.MkdirAll(meloctlConfDir, 0755)
38 |
39 | if ok, _ := fileutils.Exists(meloctlConfFile); ok {
40 | fmt.Printf("✅ [%s] Meloctl is already installed\n", meloctlConfFile)
41 | return
42 | }
43 |
44 | err := prompt.AskAll(MeloctlConfig{}, &rawMeloconf)
45 | if err != nil {
46 | fmt.Println(err)
47 | return
48 | }
49 |
50 | conf := NewMeloctlConfigFromRaw(rawMeloconf)
51 | out, err := yaml.Marshal(conf)
52 | if err != nil {
53 | fmt.Println(err)
54 | return
55 | }
56 |
57 | err = ioutil.WriteFile(meloctlConfFile, out, 0644)
58 | if err != nil {
59 | fmt.Println(err)
60 | return
61 | }
62 |
63 | fmt.Printf("✅ [%s] Meloctl has been initialized\n", meloctlConfFile)
64 | }
65 |
66 | // NewMeloctlConfigFromRaw creates a default MeloctlConfig struct
67 | func NewMeloctlConfigFromRaw(raw map[string]string) *MeloctlConfig {
68 | return &MeloctlConfig{
69 | MelodyHomeDir: raw["melody.home"],
70 | //MelodyRulesSource: raw["melody.rules.sources"],
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/internal/logging/control.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/ma111e/melody/internal/config"
5 | "github.com/ma111e/melody/internal/events"
6 | )
7 |
8 | var (
9 | // LogChan is the channel used to receive events to be logged
10 | LogChan = make(chan events.Event)
11 | )
12 |
13 | // Start starts the logging pipeline
14 | func Start(quitErrChan chan error, shutdownChan chan bool, loggerStoppedChan chan bool) {
15 | go receiveEventsForLogging(quitErrChan, shutdownChan, loggerStoppedChan)
16 | }
17 |
18 | func isIPv4(s string) bool {
19 | for i := 0; i < len(s); i++ {
20 | if s[i] == '.' {
21 | return true
22 | }
23 | }
24 | return false
25 | }
26 |
27 | func receiveEventsForLogging(quitErrChan chan error, shutdownChan chan bool, loggerStoppedChan chan bool) {
28 | defer func() {
29 | close(loggerStoppedChan)
30 | }()
31 |
32 | for {
33 | select {
34 |
35 | case ev := <-LogChan:
36 | switch ev.GetKind() {
37 | case config.HTTPKind:
38 | if isIPv4(ev.GetSourceIP()) {
39 | if _, ok := config.Cfg.DiscardProto4[config.HTTPKind]; ok {
40 | continue
41 | }
42 | } else {
43 | if _, ok := config.Cfg.DiscardProto6[config.HTTPKind]; ok {
44 | continue
45 | }
46 | }
47 | case config.HTTPSKind:
48 | if isIPv4(ev.GetSourceIP()) {
49 | if _, ok := config.Cfg.DiscardProto4[config.HTTPSKind]; ok {
50 | continue
51 | }
52 | } else {
53 | if _, ok := config.Cfg.DiscardProto6[config.HTTPSKind]; ok {
54 | continue
55 | }
56 | }
57 | }
58 | logdata, err := ev.ToLog().String()
59 | if err != nil {
60 | Warnings.Println("Failed to serialize JSON payload while writing to log file")
61 | continue
62 | }
63 |
64 | // Log to sensor file
65 | Sensor.Println(logdata)
66 |
67 | case <-quitErrChan:
68 | return
69 |
70 | case <-shutdownChan:
71 | return
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/tcp_rules.yml:
--------------------------------------------------------------------------------
1 | ok_ack:
2 | layer: tcp
3 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
4 | match:
5 | tcp.ack: 1335620269
6 |
7 | nok_ack:
8 | layer: tcp
9 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
10 | match:
11 | tcp.ack: 0
12 |
13 | ok_seq:
14 | layer: tcp
15 | id: dc65fcd2-90bc-4b00-894b-7fe936be0298
16 | match:
17 | tcp.seq: 283559482
18 |
19 | nok_seq:
20 | layer: tcp
21 | id: 4c27b748-9c53-49d7-b16f-8e58dfeaa591
22 | match:
23 | tcp.seq: 0
24 |
25 | ok_window:
26 | layer: tcp
27 | id: 48594d79-193e-4796-8fc3-33c9d3d189f7
28 | match:
29 | tcp.window: 512
30 |
31 | nok_window:
32 | layer: tcp
33 | id: 49572418-8656-46ee-b141-0b6379e09bae
34 | match:
35 | tcp.window: 0
36 |
37 | ok_flags:
38 | layer: tcp
39 | id: b533a5c4-3ac0-40ce-8c3b-08aaf1577ab5
40 | match:
41 | tcp.flags:
42 | - "FA"
43 |
44 | nok_flags:
45 | layer: tcp
46 | id: d6e3b799-3e03-46d5-b24b-c4cb0a5bbfca
47 | match:
48 | tcp.flags:
49 | - "0"
50 |
51 | ok_fragbits:
52 | layer: tcp
53 | id: a29da618-a354-4b3c-9475-824876e4c4de
54 | match:
55 | tcp.fragbits:
56 | - "D"
57 |
58 | nok_fragbits:
59 | layer: tcp
60 | id: a0e477e7-aa5c-4246-b59b-902f6ccc79c3
61 | match:
62 | tcp.fragbits:
63 | - "M"
64 |
65 | ok_dsize:
66 | layer: tcp
67 | id: dc65fcd2-90bc-4b00-894b-7fe936be0298
68 | match:
69 | tcp.dsize: 78
70 |
71 | nok_dsize:
72 | layer: tcp
73 | id: 67c669bf-585d-4935-8c0c-4d3d18d996fc
74 | match:
75 | tcp.dsize: 0
76 |
77 | ok_payload:
78 | layer: tcp
79 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
80 | match:
81 | any: true
82 | tcp.payload:
83 | is:
84 | - "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n"
85 | any: true
86 |
87 | nok_payload:
88 | layer: tcp
89 | id: ae6de878-80fc-4052-bc64-a92beb425248
90 | match:
91 | tcp.payload:
92 | is:
93 | - "nonexistent"
94 | any: true
95 |
--------------------------------------------------------------------------------
/rules/rules-available/vpn.yml:
--------------------------------------------------------------------------------
1 | CVE-2020-19781 (Citrix ADC/Gateway):
2 | layer: http
3 | meta:
4 | id: a284dbe4-88c3-4da4-b2db-42709dd85305
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "Checking or trying to exploit CVE-2020-19781"
11 | references:
12 | - "https://support.citrix.com/article/CTX267027"
13 | match:
14 | http.uri:
15 | contains:
16 | - "/vpns/"
17 | tags:
18 | cve: "cve-2020-19781"
19 | techno: "vpn"
20 | vendor: "citrix"
21 | product: "netscaler_application_delivery_controller"
22 | impact: "information_disclosure"
23 |
24 | CVE-2018-13379 (Fortinet FortiOS SSL VPN):
25 | layer: http
26 | meta:
27 | id: fd46c426-3e87-4636-9ba6-af8d24dc5633
28 | version: 1.0
29 | author: BonjourMalware
30 | status: stable
31 | created: 2020/11/07
32 | modified: 2020/11/07
33 | description: "Checking or trying to exploit CVE-2018-13379"
34 | match:
35 | http.uri:
36 | startswith:
37 | - "/remote/fgt_lang?"
38 | tags:
39 | cve: "cve-2018-13379"
40 | techno: "vpn"
41 | vendor: "fortinet"
42 | product: "fortios"
43 | impact: "information_disclosure"
44 |
45 | #CVE-2020-5135 (SonicWall VPN Portal):
46 | # layer: http
47 | # meta:
48 | # id: 7d7733e2-6047-4dc5-8bad-609da742f230
49 | # version: 1.0
50 | # author: BonjourMalware
51 | # status: stable
52 | # created: 2020/11/07
53 | # modified: 2020/11/07
54 | # description: "Checking or trying to exploit CVE-2020-5135"
55 | # references:
56 | # - "https://www.shodan.io/search?query=product%3A%22SonicWALL+firewall+http+config%22"
57 | # - "https://www.shodan.io/search?query=product%3A%22SonicWALL+SSL-VPN+http+proxy%22"
58 | # - "https://fr.tenable.com/blog/cve-2020-5135-critical-sonicwall-vpn-portal-stack-based-buffer-overflow-vulnerability"
59 | # match:
60 | #
61 | # tags:
62 | # - "cve-2020-5135"
63 | # - "techno.vpn"
64 | # - "vendor.sonicwall"
65 | # - "product.sonicos"
66 | # - "impact.rce"
67 |
--------------------------------------------------------------------------------
/cmd/meloctl/set.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "reflect"
7 |
8 | "github.com/ma111e/melody/internal/config"
9 | "github.com/ma111e/melody/internal/tagparser"
10 | "github.com/spf13/cobra"
11 | "gopkg.in/yaml.v3"
12 | )
13 |
14 | var (
15 | setCmd = &cobra.Command{
16 | Args: cobra.ExactArgs(2),
17 | Use: "set",
18 | Short: "Set a Meloctl config value by name",
19 | Long: `This subcommand is used to set a Meloctl config value by name`,
20 | Run: setConfigKey,
21 | }
22 | )
23 |
24 | func init() {
25 | RootCmd.AddCommand(setCmd)
26 | }
27 |
28 | func setConfigKey(_ *cobra.Command, args []string) {
29 | var found bool
30 | key := args[0]
31 | val := args[1]
32 |
33 | validKeys, _ := config.LoadYAMLTagsOf(MeloctlConfig{})
34 | for _, valid := range validKeys {
35 | if key == valid {
36 | found = true
37 | break
38 | }
39 | }
40 |
41 | if !found {
42 | fmt.Printf("❌ [%s] Unknown key\n", key)
43 | return
44 | }
45 |
46 | if err := setValueByYAMLTagName(meloctlConf, key, val); err != nil {
47 | fmt.Printf("❌ Error while setting value : %s\n", err)
48 | return
49 | }
50 |
51 | fmt.Printf("%s => %s \n", key, val)
52 |
53 | out, err := yaml.Marshal(meloctlConf)
54 | if err != nil {
55 | fmt.Println(err)
56 | return
57 | }
58 |
59 | err = ioutil.WriteFile(meloctlConfFile, out, 0644)
60 | if err != nil {
61 | fmt.Println(err)
62 | return
63 | }
64 |
65 | fmt.Printf("✅ [%s] Configuration file updated\n", meloctlConfFile)
66 | }
67 |
68 | func setValueByYAMLTagName(what interface{}, key string, val string) error {
69 | for i := 0; i < reflect.TypeOf(what).Elem().NumField(); i++ {
70 | tag := reflect.TypeOf(what).Elem().Field(i).Tag
71 |
72 | _, exists := tag.Lookup("yaml")
73 | if !exists {
74 | continue
75 | }
76 |
77 | tagValue, err := tagparser.ParseYamlTagValue(tag)
78 | if err != nil {
79 | return err
80 | }
81 |
82 | if key == tagValue {
83 | reflect.ValueOf(what).Elem().FieldByName(reflect.TypeOf(what).Elem().Field(i).Name).SetString(val)
84 | break
85 | }
86 | }
87 |
88 | return nil
89 | }
90 |
--------------------------------------------------------------------------------
/internal/events/helpers/layers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import "github.com/google/gopacket/layers"
4 |
5 | // IPv4Layer is a custom layer on top of the layers.IPv4 object from gopacket
6 | type IPv4Layer struct {
7 | Header *layers.IPv4
8 | }
9 |
10 | // IPv6Layer is a custom layer on top of the layers.IPv6 object from gopacket
11 | type IPv6Layer struct {
12 | Header *layers.IPv6
13 | }
14 |
15 | // GetIPv4Header returns the gopacket's layers.IPv4 layer from the custom IPv4Layer abstraction
16 | func (lay IPv4Layer) GetIPv4Header() *layers.IPv4 {
17 | return lay.Header
18 | }
19 |
20 | // GetIPv6Header returns the gopacket's layers.IPv6 layer from the custom IPv6Layer abstraction
21 | func (lay IPv6Layer) GetIPv6Header() *layers.IPv6 {
22 | return lay.Header
23 | }
24 |
25 | // UDPLayer is a custom layer on top of the layers.UDP object from gopacket
26 | type UDPLayer struct {
27 | Header *layers.UDP
28 | }
29 |
30 | // GetUDPHeader returns the gopacket's layers.UDP layer from the custom UDPLayer abstraction
31 | func (lay UDPLayer) GetUDPHeader() *layers.UDP {
32 | return lay.Header
33 | }
34 |
35 | // TCPLayer is a custom layer on top of the layers.TCP object from gopacket
36 | type TCPLayer struct {
37 | Header *layers.TCP
38 | }
39 |
40 | // GetTCPHeader returns the gopacket's layers.TCP layer from the custom TCPLayer abstraction
41 | func (lay TCPLayer) GetTCPHeader() *layers.TCP {
42 | return lay.Header
43 | }
44 |
45 | // ICMPv4Layer is a custom layer on top of the layers.ICMPv4 object from gopacket
46 | type ICMPv4Layer struct {
47 | Header *layers.ICMPv4
48 | }
49 |
50 | // GetICMPv4Header returns the gopacket's layers.ICMPv4 layer from the custom ICMPv4Layer abstraction
51 | func (lay ICMPv4Layer) GetICMPv4Header() *layers.ICMPv4 {
52 | return lay.Header
53 | }
54 |
55 | // ICMPv6Layer is a custom layer on top of the layers.ICMPv6 object from gopacket
56 | type ICMPv6Layer struct {
57 | Header *layers.ICMPv6
58 | }
59 |
60 | // GetICMPv6Header returns the gopacket's layers.ICMPv6 layer from the custom ICMPv6Layer abstraction
61 | func (lay ICMPv6Layer) GetICMPv6Header() *layers.ICMPv6 {
62 | return lay.Header
63 | }
64 |
--------------------------------------------------------------------------------
/internal/httpparser/httpparser.go:
--------------------------------------------------------------------------------
1 | package httpparser
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "github.com/ma111e/melody/internal/config"
7 | "io"
8 | "io/ioutil"
9 | "net/http"
10 | "net/http/httputil"
11 | "strconv"
12 |
13 | "github.com/c2h5oh/datasize"
14 | )
15 |
16 | // GetBodyPayload extract the body of an http.Request without striping it
17 | func GetBodyPayload(r *http.Request) ([]byte, error) {
18 | var buf bytes.Buffer
19 | var b bytes.Buffer
20 | var dest io.Writer = &b
21 |
22 | if r.Body != nil {
23 | if r.Header.Get("Content-Length") == "" {
24 | return []byte{}, nil
25 | }
26 | iContentLength, err := strconv.ParseUint(r.Header.Get("Content-Length"), 10, 64)
27 |
28 | if err != nil {
29 | return []byte{}, fmt.Errorf("request data not logged (failed to parse Content-Length as uint64 : %s)", err)
30 | }
31 |
32 | if iContentLength > config.Cfg.MaxPOSTDataSize {
33 | return []byte{}, fmt.Errorf("request data not logged (over %s : %s)", datasize.ByteSize(config.Cfg.MaxPOSTDataSize).HumanReadable(), (datasize.ByteSize(iContentLength) * datasize.B).HumanReadable())
34 | }
35 |
36 | if _, err := buf.ReadFrom(r.Body); err != nil {
37 | return []byte{}, fmt.Errorf("failed to parse request body : got error when reading from body [%s]", err.Error())
38 | }
39 |
40 | if err := r.Body.Close(); err != nil {
41 | // Send read body in such case
42 | return buf.Bytes(), fmt.Errorf("failed to parse request body : got error while closing body [%s]", err.Error())
43 | }
44 |
45 | bodyReader := ioutil.NopCloser(bytes.NewReader(buf.Bytes()))
46 |
47 | chunked := len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked"
48 |
49 | if chunked {
50 | dest = httputil.NewChunkedWriter(dest)
51 | }
52 |
53 | if _, err := io.Copy(dest, bodyReader); err != nil {
54 | // Send read body in such case
55 | return buf.Bytes(), fmt.Errorf("failed to parse request body: got error while copying the read body [%s]", err.Error())
56 | }
57 |
58 | if chunked {
59 | _ = dest.(io.Closer).Close()
60 | _, _ = io.WriteString(&b, "\r\n")
61 | }
62 | }
63 |
64 | return b.Bytes(), nil
65 | }
66 |
--------------------------------------------------------------------------------
/rules/rules-available/server.yml:
--------------------------------------------------------------------------------
1 | CVE-2020-14882 Oracle Weblogic Server RCE:
2 | layer: http
3 | meta:
4 | id: 3e1d86d8-fba6-4e15-8c74-941c3375fd3e
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/20/07
10 | description: "Checking or trying to exploit CVE-2020-14882"
11 | references:
12 | - "https://nvd.nist.gov/vuln/detail/CVE-2020-14882"
13 | match:
14 | http.uri:
15 | startswith|any|nocase:
16 | - "/console/css/"
17 | - "/console/images"
18 | contains|any|nocase:
19 | - "console.portal"
20 | - "consolejndi.portal?test_handle="
21 | tags:
22 | cve: "cve-2020-14882"
23 | vendor: "oracle"
24 | product: "weblogic"
25 | impact: "rce"
26 |
27 | CVE-2020-13942 Apache Unomi RCE:
28 | layer: http
29 | meta:
30 | id: 282560f6-e120-4b08-8b82-73bf1166fce2
31 | version: 1.0
32 | author: BonjourMalware
33 | status: experimental
34 | created: 2020/20/07
35 | modified: 2020/20/07
36 | description: "Checking or trying to exploit CVE-2020-13942"
37 | references:
38 | - "https://nvd.nist.gov/vuln/detail/CVE-2020-13942"
39 | match:
40 | http.method:
41 | is:
42 | - "POST"
43 | http.uri:
44 | is:
45 | - "/context.json"
46 | http.body:
47 | contains:
48 | - '"filters":'
49 | - '"parameterValues":'
50 | - 'getRuntime'
51 | - '"profilePropertyCondition"'
52 | tags:
53 | cve: "cve-2020-13942"
54 | vendor: "apache"
55 | product: "unomi"
56 | impact: "rce"
57 |
58 | CVE-2021-21972 VMware vSphere:
59 | layer: http
60 | meta:
61 | id: 54fb9b52-6d37-4478-91bc-dd1c85acdb2b
62 | version: 1.0
63 | author: BonjourMalware
64 | status: experimental
65 | created: 2021/03/02
66 | modified: 2021/03/02
67 | description: "Checking or trying to exploit CVE-2021-21972"
68 | references:
69 | - "https://nvd.nist.gov/vuln/detail/CVE-2021-21972"
70 | - "https://swarm.ptsecurity.com/unauth-rce-vmware/"
71 | match:
72 | http.uri:
73 | startswith|nocase:
74 | - "/ui/vropspluginui/rest/services"
75 | tags:
76 | cve: "cve-2021-21972"
77 | vendor: "vmware"
78 | product: "vsphere"
79 | impact: "rce"
80 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/http_rules.yml:
--------------------------------------------------------------------------------
1 | ok_uri:
2 | layer: http
3 | version: 1.0
4 | id: 9fac5a6d-a3c5-487a-950d-4c56a6d025ad
5 | match:
6 | http.uri:
7 | is:
8 | - "/cgi-bin/mainfunction.cgi"
9 | any: true
10 |
11 | nok_uri:
12 | layer: http
13 | version: 1.0
14 | id: 300ebc19-8f48-4fbe-a39a-9b3457b0bd96
15 | match:
16 | http.uri:
17 | is:
18 | - "/nonexistent"
19 | any: true
20 |
21 | ok_is_tls:
22 | layer: http
23 | version: 1.0
24 | id: 352e81f5-beaa-49c5-8306-87f7ba4092bd
25 | match:
26 | http.tls: false
27 |
28 | nok_is_tls:
29 | layer: http
30 | version: 1.0
31 | id: dca1a1ac-3c44-40ae-a1c0-0b8881e9f23e
32 | match:
33 | http.tls: true
34 |
35 | ok_headers:
36 | layer: http
37 | version: 1.0
38 | id: 5a819c44-bc45-4db1-9504-198a79da689d
39 | match:
40 | http.headers:
41 | is:
42 | - "X-Testing: I am a test header"
43 |
44 | nok_headers:
45 | layer: http
46 | version: 1.0
47 | id: 795e097a-662d-4839-9301-97e73bd5a4bc
48 | match:
49 | http.headers:
50 | is:
51 | - "X-Nonexistent: I do not exist"
52 |
53 | ok_body:
54 | layer: http
55 | version: 1.0
56 | id: 740055f3-1b77-4677-89ec-bcea34097593
57 | match:
58 | http.body:
59 | is:
60 | - '{"testkey": "testvalue"}'
61 |
62 | nok_body:
63 | layer: http
64 | version: 1.0
65 | id: 4adf1dc4-912c-459a-82f8-5b3aa0f20bba
66 | match:
67 | http.body:
68 | is:
69 | - "nonexistent body"
70 |
71 | ok_proto:
72 | layer: http
73 | version: 1.0
74 | id: f0b82f01-cbf7-4af7-bf26-44dc9403f0f9
75 | match:
76 | http.proto:
77 | is:
78 | - "HTTP/1.1"
79 |
80 | nok_proto:
81 | layer: http
82 | version: 1.0
83 | id: c0917358-a738-4afe-83e9-9ababa745b62
84 | match:
85 | http.proto:
86 | is:
87 | - "nonexistent proto"
88 |
89 | ok_method:
90 | layer: http
91 | version: 1.0
92 | id: b2656489-6118-4e9e-aebd-1413737666ad
93 | match:
94 | http.method:
95 | is:
96 | - "POST"
97 |
98 | nok_method:
99 | layer: http
100 | version: 1.0
101 | id: c1e40473-e250-48eb-b5ff-cf25cd4469a0
102 | match:
103 | http.method:
104 | is:
105 | - "nonexistent verb"
106 |
--------------------------------------------------------------------------------
/internal/rules/rules.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "github.com/ma111e/melody/internal/filters"
5 | "github.com/ma111e/melody/internal/logging"
6 | )
7 |
8 | // Rules abstracts an array of Rule
9 | type Rules []Rule
10 |
11 | // Rule describes a parsed Rule object, used to match against a byte array
12 | type Rule struct {
13 | Name string
14 | ID string
15 | Tags map[string]string
16 |
17 | Layer string
18 |
19 | IPProtocol *ConditionsList
20 |
21 | HTTP ParsedHTTPRule
22 | TCP ParsedTCPRule
23 | UDP ParsedUDPRule
24 | ICMPv4 ParsedICMPv4Rule
25 | ICMPv6 ParsedICMPv6Rule
26 |
27 | IPs filters.IPRules
28 | Ports filters.PortRules
29 | Metadata Metadata
30 | Additional map[string]string
31 |
32 | MatchAll bool
33 | }
34 |
35 | // NewRule creates a Rule from a RawRule
36 | func NewRule(rawRule RawRule) Rule {
37 | var portsList = filters.PortRules{
38 | WhitelistedPorts: filters.PortRanges{},
39 | BlacklistedPorts: filters.PortRanges{},
40 | }
41 |
42 | var ipsList = filters.IPRules{
43 | WhitelistedIPs: filters.IPRanges{},
44 | BlacklistedIPs: filters.IPRanges{},
45 | }
46 |
47 | ipsList.ParseRules(rawRule.Whitelist.IPs, rawRule.Blacklist.IPs)
48 | portsList.ParseRules(rawRule.Whitelist.Ports, rawRule.Blacklist.Ports)
49 |
50 | parsedIPProtocol, err := rawRule.IPProtocol.ParseList()
51 | if err != nil {
52 | logging.Errors.Printf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err)
53 | return Rule{}
54 | }
55 |
56 | rule := Rule{
57 | Tags: rawRule.Tags,
58 | IPProtocol: parsedIPProtocol,
59 | ID: rawRule.Metadata.ID,
60 | Layer: rawRule.Layer,
61 | Ports: portsList,
62 | IPs: ipsList,
63 | Metadata: rawRule.Metadata,
64 | Additional: rawRule.Additional,
65 | }
66 |
67 | return rule
68 | }
69 |
70 | // Filter is a helper filtering out one or multiple Rule according to a function returning true or false
71 | // Similar to array.filter() in python
72 | func (rules Rules) Filter(fn func(rule Rule) bool) Rules {
73 | res := Rules{}
74 |
75 | for _, rule := range rules {
76 | if fn(rule) {
77 | res = append(res, rule)
78 | }
79 | }
80 |
81 | return res
82 | }
83 |
84 | // Parse parses raw rules to create a set of rules as Rules
85 | //func (rawRules RawRules) Parse() Rules {
86 | // rules := Rules{}
87 | // for rname, rule := range rawRules {
88 | // parsedRule := rule.Parse()
89 | // parsedRule.Name = rname
90 | // rules = append(rules, parsedRule)
91 | // }
92 | //
93 | // return rules
94 | //}
95 |
--------------------------------------------------------------------------------
/internal/rules/flags_conditions.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "log"
5 | )
6 |
7 | // RawTCPFlags abstracts a string describing raw TCP flags
8 | type RawTCPFlags string
9 |
10 | // RawFragbits abstracts a string describing raw fragbits
11 | type RawFragbits string
12 |
13 | // RawTCPFlagsList abstracts an array of RawTCPFlags
14 | type RawTCPFlagsList []RawTCPFlags
15 |
16 | // RawFragbitsList abstracts an array of RawFragbits
17 | type RawFragbitsList []RawFragbits
18 |
19 | // ParseList parses a RawFragbitsList and returns a list of fragbits as an uint8 array
20 | func (list RawFragbitsList) ParseList() []*uint8 {
21 | var flagsList []*uint8
22 |
23 | if len(list) == 0 {
24 | return []*uint8{}
25 | }
26 |
27 | for _, val := range list {
28 | flagsList = append(flagsList, val.Parse())
29 | }
30 |
31 | return flagsList
32 | }
33 |
34 | // ParseList parses a RawTCPFlagsList and returns a list of tcp flags as an uint8 array
35 | func (list RawTCPFlagsList) ParseList() []*uint8 {
36 | var flagsList []*uint8
37 |
38 | if len(list) == 0 {
39 | return nil
40 | }
41 |
42 | for _, val := range list {
43 | flagsList = append(flagsList, val.Parse())
44 | }
45 |
46 | return flagsList
47 | }
48 |
49 | // Parse parses a RawTCPFlags string to return its equivalent as an uint8
50 | func (rfls RawTCPFlags) Parse() *uint8 {
51 | var flags uint8
52 |
53 | //TODO Add support for "Not" option
54 | for _, val := range rfls {
55 | switch val {
56 | case 'F':
57 | flags |= 0x01
58 | case 'S':
59 | flags |= 0x02
60 | case 'R':
61 | flags |= 0x04
62 | case 'P':
63 | flags |= 0x08
64 | case 'A':
65 | flags |= 0x10
66 | case 'U':
67 | flags |= 0x20
68 | case 'E':
69 | flags |= 0x40
70 | case 'C':
71 | flags |= 0x80
72 | case '0':
73 | flags = 0
74 | default:
75 | log.Println("Unknown TCP flag value :", val)
76 | return nil
77 | }
78 | }
79 |
80 | return &flags
81 | }
82 |
83 | // Parse parses a RawFragbits string to return its equivalent as an uint8
84 | func (rfbs RawFragbits) Parse() *uint8 {
85 | var fragbits uint8
86 |
87 | if len(rfbs) == 0 {
88 | return nil
89 | }
90 |
91 | for _, flag := range rfbs {
92 | switch flag {
93 | case 'M':
94 | fragbits |= 0x01
95 | case 'D':
96 | fragbits |= 0x02
97 | case 'R':
98 | fragbits |= 0x04
99 | default:
100 | log.Println("Unknown fragbits value :", flag)
101 | }
102 | }
103 |
104 | return &fragbits
105 | }
106 |
--------------------------------------------------------------------------------
/internal/events/icmpv4.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/ma111e/melody/internal/events/helpers"
7 | "github.com/ma111e/melody/internal/logdata"
8 |
9 | "github.com/ma111e/melody/internal/config"
10 |
11 | "github.com/google/gopacket"
12 | "github.com/google/gopacket/layers"
13 | )
14 |
15 | // ICMPv4Event describes the structure of an event generated by an ICPMv4 packet
16 | type ICMPv4Event struct {
17 | //ICMPv4Header *layers.ICMPv4
18 | LogData logdata.ICMPv4EventLog
19 | BaseEvent
20 | helpers.IPv4Layer
21 | helpers.ICMPv4Layer
22 | }
23 |
24 | // NewICMPv4Event created a new ICMPv4Event from a packet
25 | func NewICMPv4Event(packet gopacket.Packet) (*ICMPv4Event, error) {
26 | var ev = &ICMPv4Event{}
27 | ev.Kind = config.ICMPv4Kind
28 |
29 | ev.Session = "n/a"
30 | ev.Timestamp = packet.Metadata().Timestamp
31 |
32 | ICMPv4Header, _ := packet.Layer(layers.LayerTypeICMPv4).(*layers.ICMPv4)
33 | ev.ICMPv4Layer = helpers.ICMPv4Layer{Header: ICMPv4Header}
34 |
35 | IPHeader, _ := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
36 | ev.IPv4Layer = helpers.IPv4Layer{Header: IPHeader}
37 | ev.SourceIP = ev.IPv4Layer.Header.SrcIP.String()
38 | ev.Additional = make(map[string]string)
39 | ev.Tags = make(Tags)
40 |
41 | return ev, nil
42 | }
43 |
44 | // ToLog parses the event structure and generate an EventLog almost ready to be sent to the logging file
45 | func (ev ICMPv4Event) ToLog() EventLog {
46 | ev.LogData = logdata.ICMPv4EventLog{}
47 | ev.LogData.Timestamp = ev.Timestamp.Format(time.RFC3339Nano)
48 |
49 | //ev.LogData.Type = ev.Kind
50 | //ev.LogData.SourceIP = ev.SourceIP
51 | //ev.LogData.DestPort = ev.DestPort
52 | //ev.LogData.Session = ev.Session
53 | //
54 | //if len(ev.Tags) == 0 {
55 | // ev.LogData.Tags = make(map[string][]string)
56 | //} else {
57 | // ev.LogData.Tags = ev.Tags
58 | //}
59 |
60 | ev.LogData.Init(ev.BaseEvent)
61 |
62 | ev.LogData.ICMPv4 = logdata.ICMPv4LogData{
63 | TypeCode: ev.ICMPv4Layer.Header.TypeCode,
64 | Type: ev.ICMPv4Layer.Header.TypeCode.Type(),
65 | Code: ev.ICMPv4Layer.Header.TypeCode.Code(),
66 | TypeCodeName: ev.ICMPv4Layer.Header.TypeCode.String(),
67 | Checksum: ev.ICMPv4Layer.Header.Checksum,
68 | ID: ev.ICMPv4Layer.Header.Id,
69 | Seq: ev.ICMPv4Layer.Header.Seq,
70 | Payload: logdata.NewPayloadLogData(ev.ICMPv4Layer.Header.Payload, config.Cfg.MaxICMPv4DataSize),
71 | }
72 |
73 | ev.LogData.IP = logdata.NewIPv4LogData(ev.IPv4Layer)
74 | ev.LogData.Additional = ev.Additional
75 |
76 | return ev.LogData
77 | }
78 |
--------------------------------------------------------------------------------
/cmd/meloctl/root.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "path/filepath"
9 |
10 | "github.com/ma111e/melody/internal/fileutils"
11 |
12 | "github.com/ma111e/melody/internal/config"
13 | "gopkg.in/yaml.v3"
14 |
15 | "github.com/spf13/cobra"
16 | )
17 |
18 | // RootCmd represents the base command when called without any subcommands
19 | var (
20 | RootCmd = &cobra.Command{
21 | Use: "meloctl",
22 | Short: "Melody helper",
23 | Long: "Melody helper",
24 | }
25 | meloctlConf *MeloctlConfig
26 | melodyConf *config.Config
27 | meloctlConfFile string
28 | meloctlConfDir string
29 | )
30 |
31 | func init() {
32 | // Trick to prevent the "unused variable" warning message for melodyConf, which is triggered because the relationship
33 | // between the cobra.OnInitialize function and the use of melodyConf via loadConfig is not being currently detected
34 | _ = melodyConf
35 | homeDir, err := os.UserHomeDir()
36 | if err != nil {
37 | log.Fatalln("Failed to get home directory path")
38 | }
39 |
40 | meloctlConfDir = filepath.Join(homeDir, ".config", "meloctl")
41 | meloctlConfFile = filepath.Join(meloctlConfDir, "meloctl.yml")
42 |
43 | cobra.OnInitialize(loadConfig)
44 | }
45 |
46 | func loadConfig() {
47 | var err error
48 | meloctlConf = loadMeloctl()
49 | melodyConf, err = loadMelodyConfig(filepath.Join(meloctlConf.MelodyHomeDir, "config.yml"))
50 | if err != nil {
51 | log.Println(err)
52 | }
53 | }
54 |
55 | func loadMeloctl() *MeloctlConfig {
56 | var conf MeloctlConfig
57 |
58 | ok, err := fileutils.Exists(meloctlConfFile)
59 | if err != nil {
60 | fmt.Println(err)
61 | return nil
62 | }
63 |
64 | if !ok {
65 | if os.Args[1] != "init" {
66 | fmt.Println("⚠️ Meloctl config have not been initialized yet. Create a new configuration file using \"meloctl init\"")
67 | }
68 |
69 | return &conf
70 | }
71 |
72 | rawConf, err := ioutil.ReadFile(meloctlConfFile)
73 | if err != nil {
74 | fmt.Println(err)
75 | return nil
76 | }
77 |
78 | err = yaml.Unmarshal(rawConf, &conf)
79 | if err != nil {
80 | fmt.Println(err)
81 | return nil
82 | }
83 |
84 | return &conf
85 | }
86 |
87 | func loadMelodyConfig(configPath string) (*config.Config, error) {
88 | conf := config.NewConfig()
89 | cfgData, err := ioutil.ReadFile(configPath)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | if err = yaml.Unmarshal(cfgData, conf); err != nil {
95 | return nil, err
96 | }
97 |
98 | return conf, err
99 | }
100 |
--------------------------------------------------------------------------------
/internal/events/base.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "github.com/ma111e/melody/internal/loggable"
5 | "time"
6 | )
7 |
8 | // BaseEvent described the common structure to all the events generated by the received packets
9 | type BaseEvent struct {
10 | IPVersion uint
11 | Tags Tags
12 | Kind string
13 | SourceIP string
14 | DestPort uint16
15 | Session string
16 | Timestamp time.Time
17 | Additional map[string]string
18 | Event
19 | loggable.Loggable
20 | }
21 |
22 | // Tags is an abstraction of map[string]interface{} allowing for the use of a set-like structure and a more graceful
23 | // conversion to array
24 | type Tags map[string][]string
25 |
26 | // GetKind fetches the Kind of an event
27 | func (ev BaseEvent) GetKind() string {
28 | return ev.Kind
29 | }
30 |
31 | // GetSourceIP fetches the SourceIP of an event
32 | func (ev BaseEvent) GetSourceIP() string {
33 | return ev.SourceIP
34 | }
35 |
36 | // GetDestPort fetches the DestPort of an event
37 | func (ev BaseEvent) GetDestPort() uint16 {
38 | return ev.DestPort
39 | }
40 |
41 | // GetSession fetches the Session of an event
42 | func (ev BaseEvent) GetSession() string {
43 | return ev.Session
44 | }
45 |
46 | // GetTags fetches the Tags of an event
47 | func (ev BaseEvent) GetTags() map[string][]string {
48 | return ev.Tags
49 | }
50 |
51 | // AddAdditional fetches the Additional values of an event
52 | func (ev *BaseEvent) AddAdditional(add map[string]string) {
53 | for key, values := range add {
54 | ev.Additional[key] = values
55 | }
56 | }
57 |
58 | // AddTags add the given tag array to the event's tags
59 | //func (ev *BaseEvent) AddTags(tags []string) {
60 | // for _, tag := range tags {
61 | // ev.Tags[tag] = struct{}{}
62 | // }
63 | //}
64 |
65 | // AddTags add the given tag array to the event's tags
66 | func (ev *BaseEvent) AddTags(tags map[string]string) {
67 | // If the tag does not already exist in its category, add it
68 | for cat, tag := range tags {
69 | if _, ok := ev.Tags[cat]; !ok {
70 | ev.Tags[cat] = []string{tag}
71 | continue
72 | }
73 |
74 | for _, val := range ev.Tags[cat] {
75 | if val == tag {
76 | break
77 | }
78 | }
79 |
80 | ev.Tags[cat] = append(ev.Tags[cat], tag)
81 | }
82 | }
83 |
84 | ////ToInlineArray converts a Tags map to an array of its values with the keys and values merged with a '.'
85 | //func (t *Tags) ToInlineArray() []string {
86 | // var inlineTags []string
87 | //
88 | // for key, values := range *t {
89 | // for _, val := range values {
90 | // inlineTags = append(inlineTags, fmt.Sprintf("%s.%s", key, val))
91 | // }
92 | // }
93 | //
94 | // return inlineTags
95 | //}
96 |
--------------------------------------------------------------------------------
/rules/rules-available/router.yml:
--------------------------------------------------------------------------------
1 | CVE-2017-17215 (Huawei routers):
2 | layer: http
3 | meta:
4 | id: 846cef81-3e82-40ba-a8ed-54137ccea691
5 | version: 1.0
6 | author: BonjourMalware
7 | status: stable
8 | created: 2020/11/07
9 | modified: 2020/11/07
10 | description: "Checking or trying to exploit CVE-2017-17215"
11 | match:
12 | http.uri:
13 | is:
14 | - "/ctrlt/DeviceUpgrade_1"
15 | tags:
16 | cve: "cve-2017-17215"
17 | device: "router"
18 | vendor: "huawei"
19 | impact: "rce"
20 |
21 | CVE-2020-8515 (DrayTek routers):
22 | layer: http
23 | meta:
24 | id: b7f564b2-44f0-434f-bf9a-7f55c0e50463
25 | version: 1.0
26 | author: BonjourMalware
27 | status: stable
28 | created: 2020/11/07
29 | modified: 2020/11/07
30 | description: "Checking or trying to exploit CVE-2020-8515"
31 | match:
32 | http.uri:
33 | startswith:
34 | - "/cgi-bin/mainfunction.cgi"
35 | tags:
36 | cve: "cve-2020-8515"
37 | device: "router"
38 | vendor: "draytek"
39 | impact: "rce"
40 |
41 | CVE-2014-8361 (Realtek SDK Miniigd UPnP SOAP):
42 | layer: http
43 | meta:
44 | id: dd852a90-625e-416a-a69d-ad22554f9d25
45 | version: 1.0
46 | author: BonjourMalware
47 | status: stable
48 | created: 2020/11/07
49 | modified: 2020/11/07
50 | description: "Checking or trying to exploit CVE-2014-8361"
51 | match:
52 | http.uri:
53 | is:
54 | - "/picsdesc.xml"
55 | tags:
56 | cve: "cve-2014-8361"
57 | device: "router"
58 | vendor: "realtek"
59 | impact: "rce"
60 | techno: "soap"
61 |
62 | ZyXEL Eir D1000:
63 | layer: http
64 | meta:
65 | id: 98d37645-256a-48d4-a4fe-105907805417
66 | version: 1.0
67 | author: BonjourMalware
68 | status: stable
69 | created: 2020/11/07
70 | modified: 2020/11/07
71 | description: "Checking or trying to exploit a vulnerable ZyXEL Eir D1000 endpoint"
72 | match:
73 | http.uri:
74 | is:
75 | - "/UD/act?1"
76 | tags:
77 | device: "router"
78 | vendor: "zyxel"
79 | impact: "rce"
80 | techno: "soap"
81 |
82 | Linksys routers (Moon worm):
83 | layer: http
84 | meta:
85 | id: 22ac8b19-2ed3-430e-8980-fcb30d96ccb4
86 | version: 1.0
87 | author: BonjourMalware
88 | status: stable
89 | created: 2020/11/07
90 | modified: 2020/11/07
91 | description: "Checking or trying to exploit a vulnerable Lynksys router"
92 | match:
93 | http.uri:
94 | is:
95 | - "/tmUnblock.cgi"
96 | tags:
97 | device: "router"
98 | vendor: "linksys"
99 | impact: "rce"
100 | malware: "moon_worm"
101 | malware_type: "worm"
102 |
--------------------------------------------------------------------------------
/internal/rules/tagparser.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/ma111e/melody/internal/tagparser"
7 | )
8 |
9 | // LoadValidMatchKeysMap returns a map of the json keys for each of the protos structs
10 | func LoadValidMatchKeysMap() map[string]interface{} {
11 | loadFns := []func() ([]string, error){
12 | loadHTTPYamlTags,
13 | loadTCPYamlTags,
14 | loadUDPYamlTags,
15 | loadICMPv4YamlTags,
16 | loadICMPv6YamlTags,
17 | }
18 |
19 | matchKeysMap := make(map[string]interface{})
20 | for _, loadFn := range loadFns {
21 | tags, err := loadFn()
22 | if err != nil {
23 | panic(err)
24 | }
25 |
26 | for _, tag := range tags {
27 | matchKeysMap[tag] = new(interface{})
28 | }
29 | }
30 |
31 | return matchKeysMap
32 | }
33 |
34 | // Below : all the same functions with a different struct
35 | func loadHTTPYamlTags() ([]string, error) {
36 | var tags []string
37 | for i := 0; i < reflect.TypeOf(HTTPRule{}).NumField(); i++ {
38 | ruleTag := reflect.TypeOf(HTTPRule{}).Field(i).Tag
39 | tagValue, err := tagparser.ParseYamlTagValue(ruleTag)
40 | if err != nil {
41 | return tags, err
42 | }
43 | tags = append(tags, tagValue)
44 | }
45 |
46 | return tags, nil
47 | }
48 |
49 | func loadTCPYamlTags() ([]string, error) {
50 | var tags []string
51 | for i := 0; i < reflect.TypeOf(TCPRule{}).NumField(); i++ {
52 | ruleTag := reflect.TypeOf(TCPRule{}).Field(i).Tag
53 | tagValue, err := tagparser.ParseYamlTagValue(ruleTag)
54 | if err != nil {
55 | return tags, err
56 | }
57 | tags = append(tags, tagValue)
58 | }
59 |
60 | return tags, nil
61 | }
62 |
63 | func loadUDPYamlTags() ([]string, error) {
64 | var tags []string
65 | for i := 0; i < reflect.TypeOf(UDPRule{}).NumField(); i++ {
66 | ruleTag := reflect.TypeOf(UDPRule{}).Field(i).Tag
67 | tagValue, err := tagparser.ParseYamlTagValue(ruleTag)
68 | if err != nil {
69 | return tags, err
70 | }
71 | tags = append(tags, tagValue)
72 | }
73 |
74 | return tags, nil
75 | }
76 |
77 | func loadICMPv4YamlTags() ([]string, error) {
78 | var tags []string
79 | for i := 0; i < reflect.TypeOf(ICMPv4Rule{}).NumField(); i++ {
80 | ruleTag := reflect.TypeOf(ICMPv4Rule{}).Field(i).Tag
81 | tagValue, err := tagparser.ParseYamlTagValue(ruleTag)
82 | if err != nil {
83 | return tags, err
84 | }
85 | tags = append(tags, tagValue)
86 | }
87 |
88 | return tags, nil
89 | }
90 |
91 | func loadICMPv6YamlTags() ([]string, error) {
92 | var tags []string
93 | for i := 0; i < reflect.TypeOf(ICMPv6Rule{}).NumField(); i++ {
94 | ruleTag := reflect.TypeOf(ICMPv6Rule{}).Field(i).Tag
95 | tagValue, err := tagparser.ParseYamlTagValue(ruleTag)
96 | if err != nil {
97 | return tags, err
98 | }
99 | tags = append(tags, tagValue)
100 | }
101 |
102 | return tags, nil
103 | }
104 |
--------------------------------------------------------------------------------
/internal/events/icmpv6.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/gopacket"
7 | "github.com/google/gopacket/layers"
8 | "github.com/ma111e/melody/internal/config"
9 | "github.com/ma111e/melody/internal/events/helpers"
10 | "github.com/ma111e/melody/internal/logdata"
11 | )
12 |
13 | // ICMPv6Event describes the structure of an event generated by an ICPMv6 packet
14 | type ICMPv6Event struct {
15 | LogData logdata.ICMPv6EventLog
16 | BaseEvent
17 | helpers.IPv6Layer
18 | helpers.ICMPv6Layer
19 | }
20 |
21 | // NewICMPv6Event created a new ICMPv6Event from a packet
22 | func NewICMPv6Event(packet gopacket.Packet) (*ICMPv6Event, error) {
23 | var ev = &ICMPv6Event{}
24 | ev.Kind = config.ICMPv6Kind
25 |
26 | ev.Session = "n/a"
27 | ev.Timestamp = packet.Metadata().Timestamp
28 |
29 | ICMPv6Header, _ := packet.Layer(layers.LayerTypeICMPv6).(*layers.ICMPv6)
30 | ev.ICMPv6Layer = helpers.ICMPv6Layer{Header: ICMPv6Header}
31 |
32 | IPHeader, _ := packet.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
33 | ev.IPv6Layer = helpers.IPv6Layer{Header: IPHeader}
34 | ev.SourceIP = ev.IPv6Layer.Header.SrcIP.String()
35 | ev.Additional = make(map[string]string)
36 | ev.Tags = make(Tags)
37 |
38 | return ev, nil
39 | }
40 |
41 | // ToLog parses the event structure and generate an EventLog almost ready to be sent to the logging file
42 | func (ev ICMPv6Event) ToLog() EventLog {
43 | ev.LogData = logdata.ICMPv6EventLog{}
44 | //ev.LogData.Timestamp = time.Now().Format(time.RFC3339)
45 | //ev.LogData.NsTimestamp = strconv.FormatInt(time.Now().UnixNano(), 10)
46 | ev.LogData.Timestamp = ev.Timestamp.Format(time.RFC3339Nano)
47 |
48 | //ev.LogData.Type = ev.Kind
49 | //ev.LogData.SourceIP = ev.SourceIP
50 | //ev.LogData.DestPort = ev.DestPort
51 | //ev.LogData.Session = ev.Session
52 | //
53 | //if len(ev.Tags) == 0 {
54 | // ev.LogData.Tags = make(map[string][]string)
55 | //} else {
56 | // ev.LogData.Tags = ev.Tags
57 | //}
58 |
59 | ev.LogData.Init(ev.BaseEvent)
60 |
61 | ev.LogData.ICMPv6 = logdata.ICMPv6LogData{
62 | TypeCode: ev.ICMPv6Layer.Header.TypeCode,
63 | Type: ev.ICMPv6Layer.Header.TypeCode.Type(),
64 | Code: ev.ICMPv6Layer.Header.TypeCode.Code(),
65 | TypeCodeName: ev.ICMPv6Layer.Header.TypeCode.String(),
66 | Checksum: ev.ICMPv6Layer.Header.Checksum,
67 | Payload: logdata.NewPayloadLogData(ev.ICMPv6Layer.Header.Payload, config.Cfg.MaxICMPv6DataSize),
68 | }
69 |
70 | ev.LogData.IP = logdata.IPv6LogData{
71 | Version: ev.IPv6Layer.Header.Version,
72 | Length: ev.IPv6Layer.Header.Length,
73 | NextHeader: ev.IPv6Layer.Header.NextHeader,
74 | NextHeaderName: ev.IPv6Layer.Header.NextHeader.String(),
75 | TrafficClass: ev.IPv6Layer.Header.TrafficClass,
76 | FlowLabel: ev.IPv6Layer.Header.FlowLabel,
77 | HopLimit: ev.IPv6Layer.Header.HopLimit,
78 | }
79 |
80 | ev.LogData.Additional = ev.Additional
81 |
82 | return ev.LogData
83 | }
84 |
--------------------------------------------------------------------------------
/internal/rules/load.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 |
11 | "github.com/ma111e/melody/internal/logging"
12 |
13 | "github.com/ma111e/melody/internal/config"
14 |
15 | "gopkg.in/yaml.v3"
16 | )
17 |
18 | var (
19 | // GlobalRules is the global object holding all the loaded rules
20 | GlobalRules = make(map[string][]Rules)
21 | )
22 |
23 | // GlobalRawRules describes a set of RawRules
24 | type GlobalRawRules []RawRules
25 |
26 | // LoadRulesDir walks the given directory to find rule files and load them into GlobalRules
27 | func LoadRulesDir(rulesDir string) uint {
28 | var err error
29 | var globalRawRules GlobalRawRules
30 | var total uint
31 |
32 | skiplist := []string{
33 | ".gitkeep",
34 | }
35 |
36 | globalRawRules, err = ParseRulesDir(rulesDir, skiplist)
37 | if err != nil {
38 | log.Println(fmt.Sprintf("Failed to parse rule directory [%s]", rulesDir))
39 | log.Println(err)
40 | os.Exit(1)
41 | }
42 |
43 | for _, rawRules := range globalRawRules {
44 | rules := Rules{}
45 | for ruleName, rawRule := range rawRules {
46 | rule, err := rawRule.Parse()
47 | if err != nil {
48 | logging.Warnings.Println(err)
49 | continue
50 | }
51 | rule.Name = ruleName
52 |
53 | rules = append(rules, rule)
54 | }
55 |
56 | for _, proto := range config.Cfg.MatchProtocols {
57 | GlobalRules[proto] = append(GlobalRules[proto], rules.Filter(func(rule Rule) bool { return rule.Layer == proto }))
58 | }
59 | }
60 |
61 | for _, protocolRules := range GlobalRules {
62 | for _, ruleset := range protocolRules {
63 | total += uint(len(ruleset))
64 | }
65 | }
66 |
67 | return total
68 | }
69 |
70 | // ParseRulesDir walks a directory and parses each of the rule file it encounters
71 | func ParseRulesDir(rulesDir string, skiplist []string) ([]RawRules, error) {
72 | var rawRules []RawRules
73 | err := filepath.Walk(rulesDir,
74 | func(path string, info os.FileInfo, err error) error {
75 | if err != nil {
76 | return err
77 | }
78 | if info.IsDir() {
79 | return nil
80 | }
81 |
82 | for _, skipped := range skiplist {
83 | if info.Name() == skipped {
84 | return nil
85 | }
86 | }
87 |
88 | if strings.HasSuffix(path, ".yml") {
89 | log.Println("Parsing", path)
90 | parsed, err := ParseYAMLRulesFile(path)
91 | if err != nil {
92 | return fmt.Errorf("failed to read YAML rule file [%s] : %s", path, err)
93 | }
94 |
95 | rawRules = append(rawRules, parsed)
96 | } else {
97 | return fmt.Errorf("invalid rule file (wanted : .yml) : %s", path)
98 | }
99 |
100 | return nil
101 | })
102 |
103 | return rawRules, err
104 | }
105 |
106 | // ParseYAMLRulesFile is an helper that parses the given YAML file and return a set of raw rules as RawRules
107 | func ParseYAMLRulesFile(filepath string) (RawRules, error) {
108 | rawRules := RawRules{}
109 | rulesData, err := ioutil.ReadFile(filepath)
110 | if err != nil {
111 | return RawRules{}, err
112 | }
113 |
114 | if err := yaml.Unmarshal(rulesData, &rawRules); err != nil {
115 | return RawRules{}, err
116 | }
117 |
118 | return rawRules, nil
119 | }
120 |
--------------------------------------------------------------------------------
/docs/docs/installation.md:
--------------------------------------------------------------------------------
1 | To install Melody, clone the repo or grab the latest :
2 |
3 | ```bash
4 | git clone https://github.com/ma111e/melody /opt/melody
5 |
6 | cd /opt/melody
7 |
8 | go build -ldflags="-s -w -extldflags=-static" -o melody
9 | sudo setcap cap_net_raw,cap_setpcap=ep ./melody
10 |
11 | echo "Ensure net-tools is installed in order to use the 'route' command"
12 | sudo apt install net-tools
13 |
14 | echo "> Setting listening interface to \"$(route | grep '^default' | grep -o '[^ ]*$')\""
15 | sed -i "s/# listen.interface: \"lo\"/listen.interface: \"$(route | grep '^default' | grep -o '[^ ]*$')\"/g" /opt/melody/config.yml
16 | echo
17 | echo -n "Current listening interface :\n\t"
18 | grep listen.interface /opt/melody/config.yml
19 |
20 | echo "Current BPF is '$(cat /opt/melody/filter.bpf)'"
21 |
22 | # Don't forget to filter the noise by editing filter.bpf
23 | ```
24 |
25 | ## Systemd
26 |
27 | You can tweak the provided service file to use Melody with `systemd`.
28 |
29 | The file can be found in `$melody/etc/melody.service`.
30 |
31 | !!! Example
32 | ```service
33 | [Unit]
34 | Description=Melody sensor
35 | After=network-online.target
36 |
37 | [Service]
38 | Type=simple
39 | WorkingDirectory=/opt/melody
40 | ExecStart=/opt/melody/melody
41 | Restart=on-failure
42 | User=melody
43 | Group=melody
44 |
45 | [Install]
46 | WantedBy=multi-user.target
47 | ```
48 |
49 | Install it with :
50 |
51 | ```bash
52 | make service
53 | ```
54 |
55 | or
56 |
57 | ```bash
58 | sudo ln -s "$(pwd)/etc/melody.service" /etc/systemd/system/melody.service
59 | sudo systemctl daemon-reload
60 | sudo systemctl enable melody
61 | sudo systemctl status melody
62 | ```
63 |
64 | ## Supervisord
65 |
66 | You can also tweak the provided configuration file to use Melody with `supervisord`.
67 |
68 | The file can be found in `$melody/etc/melody.conf`.
69 |
70 | !!! Example
71 | ```init
72 | [program:melody]
73 | command=/opt/melody/melody
74 | directory=/opt/melody
75 | stdout_logfile=/opt/melody/melody.out
76 | stderr_logfile=/opt/melody/melody.err
77 | autostart=true
78 | autorestart=true
79 | stopasgroup=true
80 | killasgroup=true
81 | ```
82 |
83 | Install it with :
84 |
85 | ```bash
86 | make supervisor
87 | ```
88 |
89 | or
90 |
91 | ```bash
92 | sudo ln -s $(pwd)/etc/melody.conf /etc/supervisor/conf.d/melody.conf
93 | sudo supervisorctl reload
94 | sudo supervisorctl status all
95 | ```
96 |
97 | ## Uninstall
98 | Uninstall Melody by removing the log directories (default `$melody/logs`), the service files (`/etc/systemd/system/melody.service` and `/etc/supervisor/conf.d/melody.conf`) and the Melody home directory (default `/opt/melody`).
99 |
100 | !!! Example
101 | Uncomment and use these command carefully.
102 | ```bash
103 | # sudo systemctl stop melody && sudo rm /etc/systemd/system/melody.service
104 | # sudo supervisorctl stop melody && sudo rm /etc/supervisor/conf.d/melody.conf
105 | # rm -rf /opt/melody
106 | ```
107 |
108 | !!! Danger
109 | Keep in mind that removing Melody's home directory will most likely remove its logs directory as well. All logged data might be lost.
110 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: docs
2 |
3 | default: help
4 |
5 | ## certs : Create TLS certs used by the HTTPS dummy server in "var/https/certs"
6 | certs:
7 | -mkdir -p var/https/certs
8 | openssl req -x509 -subj "/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=localhost" -newkey rsa:4096 -keyout var/https/certs/key.pem -out var/https/certs/cert.pem -days 3650 -nodes
9 |
10 | ## enable_all_rules : Enable all the rule files present in ./rules/rules-available/
11 | enable_all_rules:
12 | ln -rs ./rules/rules-available/*.yml ./rules/rules-enabled/
13 |
14 | ## docker_build : Build Docker image
15 | docker_build:
16 | docker build . -t melody
17 |
18 | ## docker_run : Run local Docker image
19 | docker_run:
20 | @echo "This will only run the locally built image."
21 | @echo "MELODY_CLI example : 'export MELODY_CLI=\"-s -i lo -F 'dst port 10080'\"'"
22 | @-mkdir logs
23 | docker run \
24 | --net=host \
25 | -e "MELODY_CLI=${MELODY_CLI}" \
26 | --mount type=bind,source="$(shell pwd)/filter.bpf",target=/app/filter.bpf,readonly \
27 | --mount type=bind,source="$(shell pwd)/config.yml",target=/app/config.yml,readonly \
28 | --mount type=bind,source="$(shell pwd)/var",target=/app/var,readonly \
29 | --mount type=bind,source="$(shell pwd)/rules",target=/app/rules,readonly \
30 | --mount type=bind,source="$(shell pwd)/logs",target=/app/logs/ \
31 | melody
32 |
33 | ## docker : Build and run Docker image
34 | docker: docker_build docker_run
35 |
36 | ## docs : Deploy documentation
37 | docs:
38 | cd docs/; mkdocs gh-deploy
39 |
40 | ## run_local_stdout : Start Melody and log to stdout
41 | run_local_stdout: build
42 | ./melody -s
43 |
44 | ## build : Build and set network capabilities to start Melody without elevated privileges
45 | build:
46 | go build -ldflags="-s -w" -o melody ./cmd/melody
47 | sudo setcap cap_net_raw,cap_setpcap=ep ./melody
48 |
49 | ## cap : Set network capabilities to start Melody without elevated privileges
50 | cap:
51 | sudo setcap cap_net_raw,cap_setpcap=ep ./melody
52 |
53 | ## setup : Init meloctl, install net-tools dependency and patch listen.interface config key with the current default interface
54 | setup:
55 | @echo "Ensure net-tools is installed in order to use the 'route' command"
56 | sudo apt install net-tools
57 | @echo "> Setting listening interface to \"$(shell route | grep '^default' | awk '{print $$8; exit}')\""
58 | sed -i "s/# listen.interface: \"lo\"/listen.interface: \"$(shell route | grep '^default' | awk '{print $$8; exit}')\"/g" ./config.yml
59 | @echo
60 | @echo -n "Current listening interface :\n\t"
61 | @grep listen.interface ./config.yml
62 |
63 | @echo -n "Current BPF is :\n\t"
64 | @cat ./filter.bpf
65 |
66 | # Don't forget to filter the noise by editing ./filter.bpf
67 |
68 | ## supervisor : Create Melody's supervisor configuration
69 | supervisor:
70 | sudo ln -s $(shell pwd)/etc/melody.conf /etc/supervisor/conf.d/
71 | sudo supervisorctl reload
72 | sudo supervisorctl status all
73 |
74 | ## service : Create Melody's systemd configuration and enable start at boot
75 | service:
76 | sudo ln -s $(shell pwd)/etc/melody.service /etc/systemd/system/melody.service
77 | sudo systemctl daemon-reload && sudo systemctl enable melody
78 | sudo systemctl stop melody && sudo systemctl status melody
79 |
80 | ## help : Show this help
81 | help: Makefile
82 | @printf "\n Melody helpers\n\n"
83 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
84 | @printf ""
85 |
--------------------------------------------------------------------------------
/cmd/melody/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "os/signal"
8 | "path/filepath"
9 | "syscall"
10 |
11 | "github.com/ma111e/melody/internal/engine"
12 | "github.com/ma111e/melody/internal/rules"
13 | "github.com/ma111e/melody/internal/sensor"
14 |
15 | "github.com/ma111e/melody/internal/logging"
16 |
17 | "github.com/google/shlex"
18 | "github.com/ma111e/melody/internal/config"
19 | "github.com/pborman/getopt/v2"
20 | )
21 |
22 | var (
23 | quitErrChan = make(chan error)
24 | shutdownChan = make(chan bool)
25 | loggerStoppedChan = make(chan bool)
26 | engineStoppedChan = make(chan bool)
27 | sensorStoppedChan = make(chan bool)
28 | quitSigChan = make(chan os.Signal, 1)
29 | )
30 |
31 | func init() {
32 | signal.Notify(quitSigChan, syscall.SIGINT, syscall.SIGTERM)
33 |
34 | config.Cli.PcapFilePath = getopt.StringLong("pcap", 'r', "", "Replay a pcap file into the sensor")
35 | config.Cli.BPF = getopt.StringLong("filter", 'F', "", "Override the filter.bpf file with the specified filter")
36 | config.Cli.HomeDirPath = getopt.StringLong("home-dir", 'H', "", "Set the home directory")
37 | config.Cli.ConfigDirPath = getopt.StringLong("config-dir", 'C', "", "Set the config directory")
38 | config.Cli.ConfigFilePath = getopt.StringLong("config", 'c', "", "Path to the config file to load")
39 | config.Cli.BPFFilePath = getopt.StringLong("bpf", 'f', "", "Path to the BPF file")
40 | config.Cli.Interface = getopt.StringLong("interface", 'i', "", "Listen on the specified interface")
41 | config.Cli.Stdout = getopt.BoolLong("stdout", 's', "Output logged data to stdout instead")
42 | config.Cli.Dump = getopt.BoolLong("dump", 'd', "Output raw packet details instead of JSON")
43 | getopt.FlagLong(&config.Cli.FreeConfig, "option", 'o', "Override configuration keys")
44 | getopt.Parse()
45 |
46 | if line := os.Getenv("MELODY_CLI"); line != "" {
47 | chunks, err := shlex.Split(line)
48 | if err != nil {
49 | _, _ = fmt.Fprintf(os.Stderr, "Failed to chunk MELODY_CLI env variable")
50 | os.Exit(1)
51 | }
52 |
53 | chunks = append([]string{os.Args[0]}, chunks...)
54 | getopt.CommandLine.Parse(chunks)
55 | }
56 |
57 | config.Cfg = config.NewConfig()
58 | err := config.Cfg.Load()
59 | if err != nil {
60 | log.Println(err)
61 | os.Exit(1)
62 | }
63 |
64 | err = logging.InitLoggers()
65 | if err != nil {
66 | logging.Std.Println(err)
67 | os.Exit(1)
68 | }
69 | loaded := rules.LoadRulesDir(filepath.Join(config.Cfg.HomeDirPath, config.Cfg.RulesDir))
70 |
71 | logging.Std.Printf("Loaded %d rules\n", loaded)
72 | logging.Std.Printf("Listing on interface %s\n", config.Cfg.Interface)
73 | }
74 |
75 | func main() {
76 | logging.Std.Println("Starting loggers")
77 | logging.Start(quitErrChan, shutdownChan, loggerStoppedChan)
78 |
79 | logging.Std.Println("Starting engine")
80 | engine.Start(quitErrChan, shutdownChan, engineStoppedChan)
81 |
82 | logging.Std.Println("Starting sensor")
83 | sensor.Start(quitErrChan, shutdownChan, sensorStoppedChan)
84 |
85 | logging.Std.Println("All systems started")
86 |
87 | select {
88 | case err := <-quitErrChan:
89 | logging.Errors.Println(err)
90 | close(shutdownChan)
91 | break
92 | case <-quitSigChan:
93 | close(shutdownChan)
94 | break
95 | case <-shutdownChan:
96 | logging.Std.Println("Shutting down")
97 | break
98 | }
99 |
100 | <-sensorStoppedChan
101 | logging.Std.Println("Sensor stopped")
102 |
103 | <-engineStoppedChan
104 | logging.Std.Println("Engine stopped")
105 |
106 | <-loggerStoppedChan
107 | logging.Std.Println("Logger stopped")
108 |
109 | logging.Std.Println("Reached shutdown")
110 | }
111 |
--------------------------------------------------------------------------------
/internal/logging/loggers.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/ma111e/melody/internal/config"
10 | "github.com/natefinch/lumberjack"
11 | )
12 |
13 | var (
14 | // Sensor is the sensor logger
15 | Sensor *log.Logger
16 |
17 | // Errors is the errors logger
18 | Errors *log.Logger
19 |
20 | // Warnings is the warnings logger
21 | Warnings *log.Logger
22 |
23 | // Std is the standard logger
24 | Std *log.Logger
25 | )
26 |
27 | // InitLoggers setup the logging environment and initialize the loggers according to the loaded configuration
28 | func InitLoggers() error {
29 | Std = log.New(os.Stderr, "", log.Ldate|log.Ltime)
30 | Sensor = log.New(nil, "", 0)
31 | Errors = log.New(nil, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
32 | Warnings = log.New(nil, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
33 |
34 | sensorLogFilepath := filepath.Join(config.Cfg.LogsDir, config.Cfg.LogsSensorFile)
35 | errorsLogFilepath := filepath.Join(config.Cfg.LogsDir, config.Cfg.LogsErrorsFile)
36 |
37 | if !*config.Cli.Stdout {
38 | if _, err := os.Stat(config.Cfg.LogsDir); os.IsNotExist(err) {
39 | if err := os.Mkdir(config.Cfg.LogsDir, 0755); err != nil {
40 | return fmt.Errorf("failed to create log directory : %s", err)
41 | }
42 | }
43 |
44 | _, err := os.Stat(errorsLogFilepath)
45 | if err != nil {
46 | if os.IsNotExist(err) {
47 | if _, err := os.Create(errorsLogFilepath); err != nil {
48 | return fmt.Errorf("failed to create the error log file : %s", err)
49 | }
50 | } else {
51 | return fmt.Errorf("failed to create the error log file : %s", err)
52 | }
53 | }
54 |
55 | _, err = os.Stat(sensorLogFilepath)
56 | if err != nil {
57 | if os.IsNotExist(err) {
58 | if _, err := os.Create(sensorLogFilepath); err != nil {
59 | return fmt.Errorf("failed to create the error log file : %s", err)
60 | }
61 | } else {
62 | return fmt.Errorf("failed to create the error log file : %s", err)
63 | }
64 | }
65 |
66 | if config.Cfg.LogErrorsEnableRotation {
67 | Errors.SetOutput(&lumberjack.Logger{
68 | Filename: filepath.Join(errorsLogFilepath),
69 | MaxSize: config.Cfg.LogsErrorsMaxSize, // megabytes
70 | MaxAge: config.Cfg.LogsErrorsMaxAge, //days
71 | Compress: config.Cfg.LogsErrorsCompressRotatedLogs, // enabled by default
72 | })
73 |
74 | Warnings.SetOutput(&lumberjack.Logger{
75 | Filename: filepath.Join(errorsLogFilepath),
76 | MaxSize: config.Cfg.LogsErrorsMaxSize, // megabytes
77 | MaxAge: config.Cfg.LogsErrorsMaxAge, // days
78 | Compress: config.Cfg.LogsErrorsCompressRotatedLogs, // enabled by default
79 | })
80 | } else {
81 | errorsFile, err := os.OpenFile(errorsLogFilepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
82 | if err != nil {
83 | return fmt.Errorf("failed to open the error log file : %s", err)
84 | }
85 |
86 | Errors.SetOutput(errorsFile)
87 | Warnings.SetOutput(errorsFile)
88 | }
89 |
90 | if config.Cfg.LogSensorEnableRotation {
91 | Sensor.SetOutput(&lumberjack.Logger{
92 | Filename: filepath.Join(sensorLogFilepath),
93 | MaxSize: config.Cfg.LogsSensorMaxSize, // megabytes
94 | MaxAge: config.Cfg.LogsSensorMaxAge, // days
95 | Compress: config.Cfg.LogsSensorCompressRotatedLogs, // enabled by default
96 | })
97 | } else {
98 | sensorFile, err := os.OpenFile(sensorLogFilepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
99 | if err != nil {
100 | return fmt.Errorf("failed to open the sensor log file : %s", err)
101 | }
102 |
103 | Sensor.SetOutput(sensorFile)
104 | }
105 | } else {
106 | Errors.SetOutput(os.Stderr)
107 | Sensor.SetOutput(os.Stdout)
108 | Warnings.SetOutput(os.Stderr)
109 | }
110 |
111 | return nil
112 | }
113 |
--------------------------------------------------------------------------------
/internal/events/tcp.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/ma111e/melody/internal/events/helpers"
8 |
9 | "github.com/ma111e/melody/internal/logdata"
10 |
11 | "github.com/ma111e/melody/internal/sessions"
12 |
13 | "github.com/ma111e/melody/internal/config"
14 |
15 | "github.com/google/gopacket"
16 | "github.com/google/gopacket/layers"
17 | )
18 |
19 | // TCPEvent describes the structure of an event generated by an ICPMv4 packet
20 | type TCPEvent struct {
21 | LogData logdata.TCPEventLog
22 | BaseEvent
23 | helpers.TCPLayer
24 | helpers.IPv4Layer
25 | helpers.IPv6Layer
26 | }
27 |
28 | // NewTCPEvent created a new TCPEvent from a packet
29 | func NewTCPEvent(packet gopacket.Packet, IPVersion uint) (*TCPEvent, error) {
30 | var ev = &TCPEvent{}
31 | ev.Kind = config.TCPKind
32 | ev.IPVersion = IPVersion
33 |
34 | ev.Session = sessions.SessionMap.GetUID(packet.TransportLayer().TransportFlow().String())
35 |
36 | switch IPVersion {
37 | case 4:
38 | IPHeader, _ := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
39 | ev.IPv4Layer = helpers.IPv4Layer{Header: IPHeader}
40 | ev.SourceIP = IPHeader.SrcIP.String()
41 | case 6:
42 | IPHeader, _ := packet.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
43 | ev.IPv6Layer = helpers.IPv6Layer{Header: IPHeader}
44 | ev.SourceIP = IPHeader.SrcIP.String()
45 |
46 | }
47 |
48 | TCPHeader, _ := packet.Layer(layers.LayerTypeTCP).(*layers.TCP)
49 |
50 | ev.Timestamp = packet.Metadata().Timestamp
51 | ev.TCPLayer = helpers.TCPLayer{Header: TCPHeader}
52 | ev.DestPort = uint16(TCPHeader.DstPort)
53 |
54 | ev.Additional = make(map[string]string)
55 | ev.Tags = make(Tags)
56 |
57 | return ev, nil
58 | }
59 |
60 | // ToLog parses the event structure and generate an EventLog almost ready to be sent to the logging file
61 | func (ev TCPEvent) ToLog() EventLog {
62 | var tcpFlagsStr []string
63 | //var ipFlagsStr []string
64 |
65 | ev.LogData = logdata.TCPEventLog{}
66 | ev.LogData.Timestamp = ev.Timestamp.Format(time.RFC3339Nano)
67 | //ev.LogData.Type = ev.Kind
68 | //ev.LogData.SourceIP = ev.SourceIP
69 | //ev.LogData.DestPort = ev.DestPort
70 | //ev.LogData.Session = ev.Session
71 | //
72 | //if len(ev.Tags) == 0 {
73 | // ev.LogData.Tags = make(map[string][]string)
74 | //} else {
75 | // ev.LogData.Tags = ev.Tags
76 | //}
77 | //
78 | ev.LogData.Init(ev.BaseEvent)
79 |
80 | switch ev.IPVersion {
81 | case 4:
82 | ev.LogData.IP = logdata.NewIPv4LogData(ev.IPv4Layer)
83 | case 6:
84 | ev.LogData.IP = logdata.NewIPv6LogData(ev.IPv6Layer)
85 | }
86 |
87 | ev.LogData.TCP = logdata.TCPLogData{
88 | Window: ev.TCPLayer.Header.Window,
89 | Seq: ev.TCPLayer.Header.Seq,
90 | Ack: ev.TCPLayer.Header.Ack,
91 | DataOffset: ev.TCPLayer.Header.DataOffset,
92 | Urgent: ev.TCPLayer.Header.Urgent,
93 | Payload: logdata.NewPayloadLogData(ev.TCPLayer.Header.Payload, config.Cfg.MaxTCPDataSize),
94 | }
95 |
96 | if ev.TCPLayer.Header.FIN {
97 | tcpFlagsStr = append(tcpFlagsStr, "F")
98 | }
99 | if ev.TCPLayer.Header.SYN {
100 | tcpFlagsStr = append(tcpFlagsStr, "S")
101 | }
102 | if ev.TCPLayer.Header.RST {
103 | tcpFlagsStr = append(tcpFlagsStr, "R")
104 | }
105 | if ev.TCPLayer.Header.PSH {
106 | tcpFlagsStr = append(tcpFlagsStr, "P")
107 | }
108 | if ev.TCPLayer.Header.ACK {
109 | tcpFlagsStr = append(tcpFlagsStr, "A")
110 | }
111 | if ev.TCPLayer.Header.URG {
112 | tcpFlagsStr = append(tcpFlagsStr, "U")
113 | }
114 | if ev.TCPLayer.Header.ECE {
115 | tcpFlagsStr = append(tcpFlagsStr, "E")
116 | }
117 | if ev.TCPLayer.Header.CWR {
118 | tcpFlagsStr = append(tcpFlagsStr, "C")
119 | }
120 | if ev.TCPLayer.Header.NS {
121 | tcpFlagsStr = append(tcpFlagsStr, "N")
122 | }
123 |
124 | ev.LogData.TCP.Flags = strings.Join(tcpFlagsStr, "")
125 | ev.LogData.Additional = ev.Additional
126 |
127 | return ev.LogData
128 | }
129 |
--------------------------------------------------------------------------------
/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | ## Melody configuration file
3 | ## This file shows all supported configuration keys with their default values
4 |
5 | ##
6 | ## Logs
7 | ##
8 |
9 | ## Directory where the logs will be written
10 | # logs.dir: "logs/"
11 |
12 | ## By default, the logs are compressed (gzip) and rotated after reaching 1G, then discarded after 30 days
13 | ## You can disable the log rotation by setting "logs.sensor.rotation.enable: false"
14 | ## The minimum rotation.max_size is 1 megabyte, anything below will disable the rotation based on file size
15 | ## Supported notation : "M, G, MB, GB" (case insensitive)
16 | ## Raw number are parsed as bytes ("1240000" == "1M")
17 | ## Warning : anything below "1240000" (raw) will be parsed as 0 megabytes
18 | ## Using raw values is discouraged
19 | ## The minimum rotation.max_age is 1 day, anything below will disable the rotation based on file age
20 |
21 | ## Sensor logs
22 | # logs.sensor.file: melody.ndjson
23 | # logs.sensor.rotation.max_size: 1G
24 | # logs.sensor.rotation.max_age: 30 # days
25 | # logs.sensor.rotation.enable: true
26 | # logs.sensor.rotation.compress: true
27 |
28 | ## Error logs
29 | # logs.errors.file: melody_err.log
30 | # logs.errors.rotation.max_size: 1G
31 | # logs.errors.rotation.max_age: 30 # days
32 | # logs.errors.rotation.enable: true
33 | # logs.errors.rotation.compress: true
34 |
35 | ## Truncate the logged data in the payload/body after reaching the size limit
36 | ## In such case, the log has the "truncated" field set as "true"
37 | # logs.http.post.max_size: "10kb"
38 | # logs.tcp.payload.max_size: "10kb"
39 | # logs.udp.payload.max_size: "10kb"
40 | # logs.icmpv4.payload.max_size: "10KB"
41 | # logs.icmpv6.payload.max_size: "10KB"
42 |
43 | ##
44 | ## Rules
45 | ##
46 |
47 | ## The directory where the active rules lives
48 | # rules.dir: "rules/rules-enabled"
49 |
50 | ## Whitelist the protocols on which you want to apply rules
51 | ## Please note that the filtered protocols will still be logged
52 | ## Available values : all, http, icmp, tcp, udp, icmpv4, icmpv6
53 | # rules.match.protocols: ["all"]
54 |
55 | ##
56 | ## Listen
57 | ##
58 |
59 | ## The interface on which Melody is listening
60 | ## Listen by default on localhost
61 | ## You want to change it to your internet facing interface (wlp3s0, ens3, enp0s25, eth0...)
62 | # listen.interface: "lo"
63 |
64 | ##
65 | ## Filters
66 | ##
67 |
68 | ## Filter out specific protocols.
69 | ## Available protocols are : udp, tcp, http, https, icmp, icmpv4 (ipv4 only), icmpv6 (ipv6 only)
70 | # filters.ipv4.proto: []
71 | # filters.ipv6.proto: []
72 |
73 | ## Filter packets according to the BPF syntax (https://biot.com/capstats/bpf.html)
74 | ## The filter must start with "inbound" to filter outgoing packets
75 | # filters.bpf.file: "filter.bpf"
76 |
77 | ##
78 | ## Dummy server
79 | ##
80 |
81 | ## Configure the dummy server spawned by Melody
82 | ## A listening HTTP server is needed to capture full HTTP requests
83 | ## Use iptables to redirect all traffic to the listening port in order to catch HTTP noise on all ports
84 | # server.http.enable: true
85 | # server.http.port: 10080
86 | # server.http.dir: "var/http/serve"
87 |
88 | ## The missing_status_code is the default HTTP status code sent back if the file is not found
89 | ## A default status code of 200 can be useful to generate false positive in badly configured scanners
90 | # server.http.response.missing_status_code: 200
91 |
92 | ## Add or override specific response headers
93 | # server.http.response.headers:
94 | # Server: "Apache"
95 |
96 | ## Same for the HTTPS server
97 | ## Valid TLS certificates are needed
98 | ## They can be generated using the Makefile (make certs)
99 | # server.https.enable: true
100 | # server.https.port: 10443
101 | # server.https.dir: "var/https/serve"
102 | # server.https.crt: "var/https/certs/cert.pem"
103 | # server.https.key: "var/https/certs/key.pem"
104 | # server.https.response.missing_status_code: 200
105 | # server.https.response.headers:
106 | # Server: "Apache"
107 |
--------------------------------------------------------------------------------
/internal/events/udp.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/ma111e/melody/internal/events/helpers"
8 | "github.com/ma111e/melody/internal/logdata"
9 |
10 | "github.com/ma111e/melody/internal/config"
11 |
12 | "github.com/ma111e/melody/internal/sessions"
13 |
14 | "github.com/google/gopacket"
15 | "github.com/google/gopacket/layers"
16 | )
17 |
18 | // UDPEvent describes the structure of an event generated by an ICPMv4 packet
19 | type UDPEvent struct {
20 | LogData logdata.UDPEventLog
21 | BaseEvent
22 | helpers.UDPLayer
23 | helpers.IPv4Layer
24 | helpers.IPv6Layer
25 | }
26 |
27 | // NewUDPEvent created a new UDPEvent from a packet
28 | func NewUDPEvent(packet gopacket.Packet, IPVersion uint) (*UDPEvent, error) {
29 | var ev = &UDPEvent{}
30 | ev.Kind = config.UDPKind
31 | ev.IPVersion = IPVersion
32 |
33 | ev.Timestamp = packet.Metadata().Timestamp
34 | ev.Session = sessions.SessionMap.GetUID(packet.TransportLayer().TransportFlow().String())
35 |
36 | switch IPVersion {
37 | case 4:
38 | IPHeader, _ := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
39 | ev.IPv4Layer = helpers.IPv4Layer{Header: IPHeader}
40 | ev.SourceIP = IPHeader.SrcIP.String()
41 | case 6:
42 | IPHeader, _ := packet.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
43 | ev.IPv6Layer = helpers.IPv6Layer{Header: IPHeader}
44 | ev.SourceIP = IPHeader.SrcIP.String()
45 | }
46 |
47 | UDPHeader, _ := packet.Layer(layers.LayerTypeUDP).(*layers.UDP)
48 | ev.UDPLayer = helpers.UDPLayer{Header: UDPHeader}
49 | ev.DestPort = uint16(UDPHeader.DstPort)
50 |
51 | ev.Additional = make(map[string]string)
52 | ev.Tags = make(Tags)
53 |
54 | return ev, nil
55 | }
56 |
57 | // ToLog parses the event structure and generate an EventLog almost ready to be sent to the logging file
58 | func (ev UDPEvent) ToLog() EventLog {
59 | var ipFlagsStr []string
60 |
61 | ev.LogData = logdata.UDPEventLog{}
62 | ev.LogData.Timestamp = ev.Timestamp.Format(time.RFC3339Nano)
63 | //ev.LogData.Type = ev.Kind
64 | //ev.LogData.SourceIP = ev.SourceIP
65 | //ev.LogData.DestPort = ev.DestPort
66 | //ev.LogData.Session = ev.Session
67 | //
68 | //if len(ev.Tags) == 0 {
69 | // ev.LogData.Tags = make(map[string][]string)
70 | //} else {
71 | // ev.LogData.Tags = ev.Tags
72 | //}
73 |
74 | ev.LogData.Init(ev.BaseEvent)
75 |
76 | switch ev.IPVersion {
77 | case 4:
78 | if ev.IPv4Layer.Header.Flags&layers.IPv4EvilBit != 0 {
79 | ipFlagsStr = append(ipFlagsStr, "EV")
80 | }
81 | if ev.IPv4Layer.Header.Flags&layers.IPv4DontFragment != 0 {
82 | ipFlagsStr = append(ipFlagsStr, "DF")
83 | }
84 | if ev.IPv4Layer.Header.Flags&layers.IPv4MoreFragments != 0 {
85 | ipFlagsStr = append(ipFlagsStr, "MF")
86 | }
87 |
88 | ev.LogData.IP = logdata.IPv4LogData{
89 | Version: ev.IPv4Layer.Header.Version,
90 | IHL: ev.IPv4Layer.Header.IHL,
91 | TOS: ev.IPv4Layer.Header.TOS,
92 | Length: ev.IPv4Layer.Header.Length,
93 | ID: ev.IPv4Layer.Header.Id,
94 | FragOffset: ev.IPv4Layer.Header.FragOffset,
95 | TTL: ev.IPv4Layer.Header.TTL,
96 | Protocol: ev.IPv4Layer.Header.Protocol,
97 | Fragbits: strings.Join(ipFlagsStr, ""),
98 | }
99 |
100 | case 6:
101 | ev.LogData.IP = logdata.IPv6LogData{
102 | Version: ev.IPv6Layer.Header.Version,
103 | Length: ev.IPv6Layer.Header.Length,
104 | NextHeader: ev.IPv6Layer.Header.NextHeader,
105 | NextHeaderName: ev.IPv6Layer.Header.NextHeader.String(),
106 | TrafficClass: ev.IPv6Layer.Header.TrafficClass,
107 | FlowLabel: ev.IPv6Layer.Header.FlowLabel,
108 | HopLimit: ev.IPv6Layer.Header.HopLimit,
109 | }
110 | }
111 |
112 | ev.LogData.UDP = logdata.UDPLogData{
113 | Payload: logdata.NewPayloadLogData(ev.UDPLayer.Header.Payload, config.Cfg.MaxUDPDataSize),
114 | Length: ev.UDPLayer.Header.Length,
115 | Checksum: ev.UDPLayer.Header.Checksum,
116 | }
117 |
118 | ev.LogData.Additional = ev.Additional
119 |
120 | return ev.LogData
121 | }
122 |
--------------------------------------------------------------------------------
/internal/rules/testing.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "fmt"
5 | "github.com/google/gopacket"
6 | "github.com/google/gopacket/layers"
7 | "github.com/google/gopacket/pcap"
8 | "github.com/ma111e/melody/internal/events"
9 | "os"
10 | )
11 |
12 | var (
13 | assetsBasePath string
14 | )
15 |
16 | func init() {
17 | assetsBasePath = "test_resources"
18 | }
19 |
20 | // ReadRawTCPPacketsFromPcap is an helper that reads raw TCP packets from the specified pcap file
21 | func ReadRawTCPPacketsFromPcap(pcapfile string) ([]gopacket.Packet, error) {
22 | var packets []gopacket.Packet
23 | _, rawPackets, err := ReadPacketsFromPcap(pcapfile, layers.IPProtocolTCP, true)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | for _, val := range rawPackets {
29 | packets = append(packets, val.(gopacket.Packet))
30 | }
31 | return packets, nil
32 | }
33 |
34 | // ReadPacketsFromPcap is an helper that reads packets from the specified pcap file and returns them as an array of Event
35 | func ReadPacketsFromPcap(pcapfile string, filter layers.IPProtocol, raw bool) ([]events.Event, []gopacket.Packet, error) {
36 | var Events []events.Event
37 | var rawPackets []gopacket.Packet
38 | var ret []events.Event
39 | var rawRet []gopacket.Packet
40 | pcapfilePath := MakeAssetFullPath(pcapfile)
41 |
42 | f, err := os.Open(pcapfilePath)
43 | if err != nil {
44 | return []events.Event{}, []gopacket.Packet{}, err
45 | }
46 | handle, err := pcap.OpenOfflineFile(f)
47 | if err != nil {
48 | return []events.Event{}, []gopacket.Packet{}, err
49 | }
50 |
51 | src := gopacket.NewPacketSource(handle, handle.LinkType())
52 | in := src.Packets()
53 |
54 | loop:
55 | for {
56 | packet := <-in
57 | if packet == nil {
58 | break loop
59 | }
60 |
61 | if _, ok := packet.NetworkLayer().(*layers.IPv4); ok {
62 | if packet.NetworkLayer().(*layers.IPv4).Protocol == filter {
63 | if raw {
64 | rawPackets = append(rawPackets, packet)
65 | } else {
66 | switch filter {
67 | case layers.IPProtocolICMPv4:
68 | ev, err := events.NewICMPv4Event(packet)
69 | if err != nil {
70 | return []events.Event{}, []gopacket.Packet{}, err
71 | }
72 |
73 | Events = append(Events, ev)
74 |
75 | case layers.IPProtocolUDP:
76 | ev, err := events.NewUDPEvent(packet, 4)
77 | if err != nil {
78 | return []events.Event{}, []gopacket.Packet{}, err
79 | }
80 |
81 | Events = append(Events, ev)
82 |
83 | case layers.IPProtocolTCP:
84 | ev, err := events.NewTCPEvent(packet, 4)
85 | if err != nil {
86 | return []events.Event{}, []gopacket.Packet{}, err
87 | }
88 |
89 | Events = append(Events, ev)
90 |
91 | default:
92 | continue loop
93 | }
94 | }
95 | }
96 | } else if _, ok := packet.NetworkLayer().(*layers.IPv6); ok {
97 | if packet.NetworkLayer().(*layers.IPv6).NextHeader == filter {
98 | switch filter {
99 | case layers.IPProtocolICMPv6:
100 | ev, err := events.NewICMPv6Event(packet)
101 | if err != nil {
102 | return []events.Event{}, []gopacket.Packet{}, err
103 | }
104 |
105 | Events = append(Events, ev)
106 |
107 | default:
108 | continue loop
109 | }
110 | }
111 | }
112 | }
113 |
114 | // I'm so lazy
115 | if raw {
116 | rawRet = make([]gopacket.Packet, len(rawPackets))
117 | copy(rawRet, rawPackets)
118 | }
119 |
120 | ret = make([]events.Event, len(Events))
121 | copy(ret, Events)
122 |
123 | return ret, rawRet, nil
124 | }
125 |
126 | // LoadRuleFile is an helper that parses the rule file at the given path and returns a rule set
127 | func LoadRuleFile(rulefile string) (map[string]Rule, error) {
128 | ruleset := make(map[string]Rule)
129 | rulefilePath := MakeAssetFullPath(rulefile)
130 | rawRules, err := ParseYAMLRulesFile(rulefilePath)
131 | if err != nil {
132 | return map[string]Rule{}, err
133 | }
134 | for name, rawRule := range rawRules {
135 | ruleset[name], err = rawRule.Parse()
136 | if err != nil {
137 | return ruleset, err
138 | }
139 | }
140 |
141 | return ruleset, nil
142 | }
143 |
144 | // MakeAssetFullPath is an helper that returns the path to the tests resources as defined by the assetsBasePath variable
145 | func MakeAssetFullPath(path string) string {
146 | return fmt.Sprintf("%s/%s", assetsBasePath, path)
147 | }
148 |
--------------------------------------------------------------------------------
/internal/rules/test_resources/logic_flow_rules.yml:
--------------------------------------------------------------------------------
1 | # Default matching mode at rule level is all
2 |
3 | ok_any_sub:
4 | layer: tcp
5 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
6 | match:
7 | tcp.payload:
8 | is:
9 | - "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n"
10 | contains:
11 | - "nonexistent"
12 | any: true
13 |
14 | nok_any_sub:
15 | layer: tcp
16 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
17 | match:
18 | tcp.payload:
19 | contains:
20 | - "nonexistent"
21 | - "neither"
22 | any: true
23 |
24 | ok_all_sub:
25 | layer: tcp
26 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
27 | match:
28 | tcp.payload:
29 | startswith:
30 | - "GET"
31 | contains:
32 | - "localhost:8080"
33 | endswith:
34 | - "\n\r\n"
35 |
36 | nok_all_sub:
37 | layer: tcp
38 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
39 | match:
40 | tcp.payload:
41 | startswith:
42 | - "GET"
43 | contains:
44 | - "localhost:8080"
45 | endswith:
46 | - "wrong end"
47 |
48 | ok_all_upper:
49 | layer: tcp
50 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
51 | match:
52 | tcp.payload:
53 | startswith:
54 | - "GET"
55 | any: true
56 | tcp.flags:
57 | - "AP"
58 |
59 | nok_all_upper:
60 | layer: tcp
61 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
62 | match:
63 | tcp.payload:
64 | startswith:
65 | - "GET"
66 | any: true
67 | tcp.flags:
68 | - "0"
69 |
70 | ok_any_upper:
71 | layer: tcp
72 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
73 | match:
74 | tcp.payload:
75 | startswith:
76 | - "GET"
77 | tcp.flags:
78 | - "0"
79 | any: true
80 |
81 | nok_any_upper:
82 | layer: tcp
83 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
84 | match:
85 | any: true
86 | tcp.payload:
87 | startswith:
88 | - "nonexistent"
89 | any: true
90 | tcp.flags:
91 | - "0"
92 |
93 | ok_any_upper_mixed:
94 | layer: tcp
95 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
96 | match:
97 | any: true
98 | tcp.payload:
99 | startswith:
100 | - "GET"
101 | contains:
102 | - "nonexistent"
103 | tcp.flags:
104 | - "PA"
105 |
106 | nok_any_upper_mixed:
107 | layer: tcp
108 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
109 | match:
110 | any: true
111 | tcp.payload:
112 | startswith:
113 | - "GET"
114 | contains:
115 | - "nonexistent"
116 | tcp.flags:
117 | - "0"
118 |
119 | ok_all_upper_mixed:
120 | layer: tcp
121 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
122 | match:
123 | tcp.payload:
124 | startswith:
125 | - "GET"
126 | contains:
127 | - "localhost"
128 | tcp.flags:
129 | - "PA"
130 |
131 | nok_all_upper_mixed:
132 | layer: tcp
133 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
134 | match:
135 | tcp.payload:
136 | startswith:
137 | - "GET"
138 | contains:
139 | - "nonexistent"
140 | tcp.flags:
141 | - "PA"
142 |
143 | ok_all_all_full_mixed:
144 | layer: tcp
145 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
146 | match:
147 | tcp.payload:
148 | startswith:
149 | - "GET"
150 | contains:
151 | - "localhost"
152 | - "curl"
153 | tcp.flags:
154 | - "PA"
155 |
156 | nok_all_all_full_mixed:
157 | layer: tcp
158 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
159 | match:
160 | tcp.payload:
161 | startswith:
162 | - "GET"
163 | contains:
164 | - "localhost"
165 | - "nonexistent"
166 | tcp.flags:
167 | - "PA"
168 |
169 | ok_all_any_full_mixed:
170 | layer: tcp
171 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
172 | match:
173 | any: true
174 | tcp.payload:
175 | startswith:
176 | - "GET"
177 | contains:
178 | - "localhost"
179 | - "curl"
180 | tcp.flags:
181 | - "0"
182 |
183 | nok_all_any_full_mixed:
184 | layer: tcp
185 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
186 | match:
187 | any: true
188 | tcp.payload:
189 | startswith:
190 | - "GET"
191 | contains:
192 | - "neither"
193 | - "nonexistent"
194 | tcp.flags:
195 | - "0"
196 |
197 | ok_any_any_full_mixed:
198 | layer: tcp
199 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
200 | match:
201 | any: true
202 | tcp.payload:
203 | startswith:
204 | - "nonexistent"
205 | contains:
206 | - "localhost"
207 | - "curl"
208 | any: true
209 | tcp.flags:
210 | - "0"
211 |
212 | nok_any_any_full_mixed:
213 | layer: tcp
214 | id: 93e28e66-b3e3-4a28-8b27-be50269c84a0
215 | match:
216 | any: true
217 | tcp.payload:
218 | startswith:
219 | - "nonexistent"
220 | contains:
221 | - "neither"
222 | - "nonexistent"
223 | any: true
224 | tcp.flags:
225 | - "0"
226 |
--------------------------------------------------------------------------------
/internal/meloctl/prompt/prompt.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 | "reflect"
8 | "strings"
9 |
10 | "github.com/fatih/structtag"
11 |
12 | "github.com/manifoldco/promptui"
13 | )
14 |
15 | // AskAll prompts the user for each property with a 'pretty' tag
16 | func AskAll(what interface{}, target *map[string]string) error {
17 | for i := 0; i < reflect.TypeOf(what).NumField(); i++ {
18 | var choices []string
19 |
20 | tag := reflect.TypeOf(what).Field(i).Tag
21 | parsedTag, err := structtag.Parse(string(tag))
22 | if err != nil {
23 | return err
24 | }
25 |
26 | yamlTag, err := parsedTag.Get("yaml")
27 | if err != nil {
28 | return err
29 | }
30 |
31 | // iterate over all tags
32 | prettyTag, _ := parsedTag.Get("pretty")
33 |
34 | if prettyTag == nil {
35 | continue
36 | }
37 |
38 | choicesTag, _ := parsedTag.Get("choices")
39 | if choicesTag != nil {
40 | choices = strings.Split(choicesTag.Value(), ",")
41 | }
42 |
43 | validateModeTag, _ := parsedTag.Get("validate")
44 | hintTag, _ := parsedTag.Get("hint")
45 |
46 | prompt := prettyTag.Value()
47 | if hintTag != nil {
48 | prompt = fmt.Sprintf("%s (%s)", prettyTag, hintTag.Value())
49 | }
50 |
51 | defaultVal := (*target)[yamlTag.Value()]
52 |
53 | if len(choices) > 0 {
54 | _, res, err := askSelect(prompt, choices, defaultVal)
55 | if err != nil {
56 | //log.Fatalln(err.Error())
57 | return err
58 | }
59 | (*target)[yamlTag.Value()] = res
60 | continue
61 | }
62 |
63 | if validateModeTag != nil {
64 | validateFn, ok := validatorsMap[validateModeTag.Value()]
65 | if !ok {
66 | //log.Fatalln(fmt.Errorf("unknown validator : %s", validateModeTag.Value()))
67 | return fmt.Errorf("unknown validator : %s", validateModeTag.Value())
68 | }
69 |
70 | res, err := askString(prompt, defaultVal, &validateFn)
71 | if err != nil {
72 | //log.Fatalln(err.Error())
73 | return err
74 | }
75 | (*target)[yamlTag.Value()] = res
76 | continue
77 | }
78 |
79 | res, err := askString(prompt, defaultVal, nil)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | (*target)[yamlTag.Value()] = res
85 | }
86 |
87 | return nil
88 | }
89 |
90 | //func askStringArray(what string, defaultVal string, validateFn *func(candidate string) error) ([]string, error) {
91 | // var answers []string
92 | //
93 | // answer, err := askString(what, defaultVal, validateFn)
94 | // if err != nil {
95 | // return answers, err
96 | // }
97 | // answers = append(answers, answer)
98 | //
99 | // for {
100 | // ok, err := AskConfirmation("Add another one ?", true)
101 | // if err != nil {
102 | // return answers, err
103 | // }
104 | // if !ok {
105 | // break
106 | // }
107 | //
108 | // answer, err := askString(what, defaultVal, validateFn)
109 | // if err != nil {
110 | // return answers, err
111 | // }
112 | // answers = append(answers, answer)
113 | // }
114 | //
115 | // return answers, nil
116 | //}
117 |
118 | func askSelect(what string, choices []string, defaultVal string) (int, string, error) {
119 | var found bool
120 | var val string
121 | var idx int
122 | var cursorPos int
123 |
124 | for idx, val = range choices {
125 | if defaultVal == val {
126 | found = true
127 | break
128 | }
129 | }
130 |
131 | if found {
132 | cursorPos = idx
133 | }
134 |
135 | prompt := promptui.Select{
136 | Label: what,
137 | Items: choices,
138 | CursorPos: cursorPos,
139 | }
140 |
141 | return prompt.Run()
142 | }
143 |
144 | //
145 | //func askString(what string, defaultVal string) (string, error) {
146 | // prompt := promptui.Prompt{
147 | // Label: what,
148 | // Default: defaultVal,
149 | // AllowEdit: true,
150 | // }
151 | //
152 | // return prompt.Run()
153 | //}
154 |
155 | //
156 | //func askStringArray(what string, defaultVal []string) (string, error) {
157 | //
158 | //
159 | // prompt := promptui.Prompt{
160 | // Label: what,
161 | // Default: defaultVal,
162 | // AllowEdit: true,
163 | // }
164 | //
165 | // return prompt.Run()
166 | //}
167 |
168 | func askString(what string, defaultVal string, validateFn *func(candidate string) error) (string, error) {
169 | var res string
170 | var err error
171 |
172 | prompt := promptui.Prompt{
173 | Label: what,
174 | Default: defaultVal,
175 | AllowEdit: true,
176 | }
177 |
178 | if validateFn != nil {
179 | prompt.Validate = *validateFn
180 | }
181 |
182 | res, err = prompt.Run()
183 | if err != nil {
184 | return res, err
185 | }
186 |
187 | return res, err
188 | }
189 |
190 | // AskConfirmation prompts the user for confirmation
191 | func AskConfirmation(what string, defaultChoice bool) (bool, error) {
192 | reader := bufio.NewReader(os.Stdin)
193 | loop := true
194 |
195 | defaultIndicator := "[Y/n]"
196 | choice := defaultChoice
197 | if !defaultChoice {
198 | defaultIndicator = "[y/N]"
199 | }
200 |
201 | for loop {
202 | fmt.Printf("%s %s : ", what, defaultIndicator)
203 | res, err := reader.ReadString('\n')
204 | if err != nil {
205 | return false, err
206 | }
207 |
208 | res = strings.ToLower(strings.TrimSpace(res))
209 |
210 | if res == "y" || res == "yes" {
211 | choice = true
212 | loop = false
213 | } else if res == "n" || res == "no" {
214 | choice = false
215 | loop = false
216 | } else if res == "" {
217 | loop = false
218 | }
219 | }
220 | return choice, nil
221 | }
222 |
--------------------------------------------------------------------------------
/docs/docs/meloctl.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Meloctl is a helper program that streamlines the use of Melody and its ecosystem by providing features such as validation checks for Melody's configuration and rule files.
4 |
5 | ```
6 | Melody helper
7 |
8 | Usage:
9 | meloctl [command]
10 |
11 | Available Commands:
12 | config Interact with a Melody config file
13 | get Get a Meloctl config value by name
14 | help Help about any command
15 | init Create Meloctl config
16 | rule Handle Melody rule files
17 | set Set a Meloctl config value by name
18 |
19 | Flags:
20 | -h, --help help for meloctl
21 |
22 | Use "meloctl [command] --help" for more information about a command.
23 | ```
24 |
25 | ## Initilization
26 |
27 | For better user experience, you'll need to store basic information such as Melody's home dir in a configuration file before starting.
28 |
29 | To do so, run `meloctl init` to start the interactive prompt.
30 |
31 | ## Features
32 | ### config
33 | #### check
34 |
35 | Check a rule file or a directory containing multiple files
36 |
37 | Example :
38 |
39 | ```
40 | $ ./meloctl config check ./config.yml
41 | ✅ [./config.yml]: OK
42 | ```
43 |
44 | ### rule
45 | #### check
46 |
47 | Validate the YAML syntax and look for unknown properties or fields.
48 |
49 | ```
50 | $ ./meloctl rule check ./rules/rules-available
51 | ❌ [rules/rules-available/cms.yml]: unknown property 'http.uri|nonexistent'
52 | ✅ [rules/rules-available/microsoft.yml]: OK
53 | ❌ [rules/rules-available/nas.yml]: yaml: line 2: did not find expected key
54 | ✅ [rules/rules-available/rdp.yml]: OK
55 | ✅ [rules/rules-available/router.yml]: OK
56 | ✅ [rules/rules-available/server.yml]: OK
57 | ✅ [rules/rules-available/vpn.yml]: OK
58 | ✅ [rules/rules-available/web.yml]: OK
59 | ```
60 |
61 | #### init
62 |
63 | Bootstrap a rule with an automatically pre-filled template.
64 |
65 | Usage :
66 |
67 | ```
68 | Usage:
69 | meloctl rule init [flags]
70 |
71 | Flags:
72 | -a, --author string Author field for new rule (default "Changeme")
73 | -d, --description string Description field for new rule
74 | -f, --force Do not ask permission to overwrite if a rule already defined
75 | -h, --help help for init
76 | -i, --interactive Ask for each parameter for the new rule
77 | -l, --layer string Layer field for new rule (default "http")
78 | -n, --name string Name field for new rule (default "Changeme")
79 | -r, --references stringArray References fields new rule
80 | -s, --status string Status field for new rule (default "experimental")
81 | -t, --tags stringToString Tags fields for new rule (default [])
82 |
83 | ```
84 |
85 | Default template :
86 |
87 | ```
88 | $ ./meloctl rule init demo.yml
89 | Writing :
90 | Changeme:
91 | layer: http
92 | meta:
93 | version: "1.0"
94 | id: 6ddbbfaa-72c1-41d8-bb78-34111286a8d2
95 | author: Changeme
96 | status: experimental
97 | created: 2021/04/19
98 | modified: 2021/04/19
99 | description: ""
100 | match:
101 | http.uri:
102 | contains|nocase:
103 | - ""
104 | endswith:
105 | - ""
106 | is|regex:
107 | - ""
108 | startswith|any:
109 | - ""
110 | references: []
111 | tags: {}
112 |
113 | ✅ [/opt/melody/demo.yml]: Rule file created
114 | ```
115 |
116 | You can use the interactive mode (`-i`), give specific values, or even mix both :
117 |
118 | ```
119 | $ ./meloctl rule init demo.yml -i --name "Demo rule" --status testing --tag "purpose=demo" --tag "teapot.state=empty"
120 | Use the arrow keys to navigate: ↓ ↑ → ←
121 | ? Layer:
122 | ▸ http
123 | icmp
124 | tcp
125 | udp
126 | ip
127 | ✔ http
128 | ✔ Version: 1.0
129 | Author: Changeme
130 | Use the arrow keys to navigate: ↓ ↑ → ←
131 | ? Status:
132 | stable
133 | experimental
134 | ▸ testing
135 | ✔ testing
136 | Created: 2021/04/19
137 | Modified: 2021/04/19
138 | ✔ Description: This is a demo rule
139 |
140 | Writing :
141 | Demo rule:
142 | layer: http
143 | meta:
144 | version: "1.0"
145 | id: 8738f81c-35d4-45f0-b553-c9d9c8993e4c
146 | author: Changeme
147 | status: testing
148 | created: 2021/04/19
149 | modified: 2021/04/19
150 | description: ""
151 | match:
152 | http.uri:
153 | contains|nocase:
154 | - ""
155 | endswith:
156 | - ""
157 | is|regex:
158 | - ""
159 | startswith|any:
160 | - ""
161 | references: []
162 | tags:
163 | purpose: demo
164 | teapot.state: empty
165 |
166 | ✅ [/opt/melody/demo.yml]: Rule file created
167 | ```
168 |
169 | #### add
170 |
171 | This command will do the same as `init`, except the new rule will be appended to the specified file.
172 |
173 | ### init
174 |
175 | ```
176 | $ ./meloctl init
177 | Melody home directory: /opt/melody
178 | ✅ [~/.config/meloctl/meloctl.yml] Meloctl has been initialized
179 | ```
180 |
181 | ### get
182 |
183 | ```
184 | $ ./meloctl get melody.home
185 | melody.home => /opt/melody
186 | ```
187 |
188 | ### set
189 |
190 | ```
191 | $ ./meloctl set melody.home /opt/melody
192 | melody.home => /opt/melody
193 | ✅ [~/.config/meloctl/meloctl.yml] Configuration file updated
194 | ```
--------------------------------------------------------------------------------
/internal/rules/conditions_test.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "testing"
7 | )
8 |
9 | func TestParseHybridPattern(t *testing.T) {
10 | tests := []struct {
11 | OkSrc []string
12 | NokSrc []string
13 | OkDst [][]byte
14 | NokDst [][]byte
15 | ErrSrc []string
16 | ErrDst []string
17 | }{
18 | {
19 | OkSrc: []string{
20 | "abcd1234abcd",
21 | "abcd1234|0d0a|abcd",
22 | "abcd1234|0d 0a|abcd",
23 | "abcd1234|0d 0a|",
24 | "|0d 0a|abcd",
25 | },
26 | OkDst: [][]byte{
27 | {97, 98, 99, 100, 49, 50, 51, 52, 97, 98, 99, 100},
28 | {97, 98, 99, 100, 49, 50, 51, 52, 13, 10, 97, 98, 99, 100},
29 | {97, 98, 99, 100, 49, 50, 51, 52, 13, 10, 97, 98, 99, 100},
30 | {97, 98, 99, 100, 49, 50, 51, 52, 13, 10},
31 | {13, 10, 97, 98, 99, 100},
32 | },
33 | NokSrc: []string{
34 | "abcd1234|0b0a|abcd",
35 | },
36 | NokDst: [][]byte{
37 | {97, 98, 99, 100, 49, 50, 51, 52, 97, 98, 99, 100},
38 | },
39 | ErrSrc: []string{
40 | "abcd12340d0a|abcd",
41 | "abcd12340d|0x0a 0x0d|abcd",
42 | },
43 | ErrDst: []string{
44 | fmt.Sprintf("failed to parse hybrid pattern : uneven number of hex delimiter (\"|\") in %s", "abcd12340d0a|abcd"),
45 | fmt.Sprintf("failed to parse hybrid pattern : [%s] in %s", "encoding/hex: invalid byte: U+0078 'x'", "abcd12340d|0x0a 0x0d|abcd"),
46 | },
47 | },
48 | }
49 |
50 | for _, suite := range tests {
51 | for idx, val := range suite.OkSrc {
52 | parsed, err := ParseHybridPattern([]byte(val))
53 | if err != nil {
54 | t.Error(val, ":", err, "FAILED")
55 | t.Fail()
56 | continue
57 | }
58 | if !bytes.Equal(parsed, suite.OkDst[idx]) {
59 | t.Error(val, "FAILED")
60 | t.Fail()
61 | }
62 | }
63 |
64 | for idx, val := range suite.NokSrc {
65 | parsed, err := ParseHybridPattern([]byte(val))
66 | if err != nil {
67 | t.Error(val, ":", err, "FAILED")
68 | t.Fail()
69 | continue
70 | }
71 | if bytes.Equal(parsed, suite.NokDst[idx]) {
72 | t.Error(val, "FAILED")
73 | t.Fail()
74 | }
75 | }
76 |
77 | for idx, val := range suite.ErrSrc {
78 | parsed, err := ParseHybridPattern([]byte(val))
79 | if err == nil {
80 | t.Error(val, "FAILED : got no error")
81 | t.Fail()
82 | continue
83 | }
84 | if parsed != nil {
85 | t.Error(val, "FAILED : parsed value is not empty")
86 | t.Fail()
87 | continue
88 | }
89 |
90 | if err.Error() != suite.ErrDst[idx] {
91 | t.Error(val, ":", err, "FAILED")
92 | t.Fail()
93 | }
94 |
95 | }
96 | }
97 | }
98 |
99 | func TestParseOptions(t *testing.T) {
100 | var cond Conditions
101 | emptyOption := Options{}
102 |
103 | tests := []struct {
104 | OkSrc []string
105 | NokSrc []string
106 | OkDst []Options
107 | NokDst []Options
108 | ErrSrc []string
109 | ErrDst []string
110 | }{
111 | {
112 | OkSrc: []string{
113 | "contains",
114 | "any|contains",
115 | "contains|any",
116 | "endswith|any",
117 | "startswith|any",
118 | "is|any",
119 | "contains|any|regex|nocase",
120 | },
121 | OkDst: []Options{
122 | {
123 | All: true,
124 | Contains: true,
125 | },
126 | {
127 | All: false,
128 | Contains: true,
129 | },
130 | {
131 | All: false,
132 | Contains: true,
133 | },
134 | {
135 | All: false,
136 | Endswith: true,
137 | },
138 | {
139 | All: false,
140 | Startswith: true,
141 | },
142 | {
143 | All: false,
144 | Is: true,
145 | },
146 | {
147 | All: false,
148 | Contains: true,
149 | Regex: true,
150 | Nocase: true,
151 | },
152 | },
153 | NokSrc: []string{
154 | "any|contains",
155 | "contains",
156 | },
157 | NokDst: []Options{
158 | {
159 | All: false,
160 | }, {
161 | All: true,
162 | },
163 | },
164 | ErrSrc: []string{
165 | "contains|is",
166 | "contains|nonexistent",
167 | "",
168 | },
169 | ErrDst: []string{
170 | fmt.Sprintf("options httpparser failed for condition '%s' : there can only be one of
3 | Monitor the Internet's background noiseMelody
4 |
81 |
82 |
83 |
http.uri:| 7 | |`http.body`|*complex*|
contains:
- "/console/css/%2e"
http.body:| 8 | |`http.headers`|*complex*|
contains:
- "I made a discovery today."
http.headers:| 9 | |`http.method`|*complex*|
is:
- "User-agent: Mozilla/5.0 zgrab/0.x"
http.method:| 10 | |`http.proto`|*complex*|
is:
- "POST"
http.proto:| 11 | |`http.tls`|*bool*|
is:
- "HTTP/1.1"
false| 12 | 13 | !!! Important 14 | HTTP being an application protocol, the full packet is assembled from multiple frames and thus does not have its transport information embedded. 15 | 16 | However, the reassembled packet data share its session with the TCP frames it comes from. You can link them together by looking up the session. 17 | 18 | !!! Note 19 | HTTPS packets are captured via the webserver and not reassembled : they have their own session and are **not** linked with the source frames. 20 | 21 | ### Log data 22 | 23 | !!! Example 24 | ```json 25 | { 26 | "http": { 27 | "verb": "POST", 28 | "proto": "HTTP/1.1", 29 | "uri": "/", 30 | "src_port": 51746, 31 | "dst_host": "127.0.0.1", 32 | "user_agent": "curl/7.58.0", 33 | "headers": { 34 | "Accept": "*/*", 35 | "Content-Length": "14", 36 | "Content-Type": "application/x-www-form-urlencoded", 37 | "User-Agent": "curl/7.58.0" 38 | }, 39 | "headers_keys": [ 40 | "User-Agent", 41 | "Accept", 42 | "Content-Length", 43 | "Content-Type" 44 | ], 45 | "headers_values": [ 46 | "curl/7.58.0", 47 | "*/*", 48 | "14", 49 | "application/x-www-form-urlencoded" 50 | ], 51 | "errors": null, 52 | "body": { 53 | "content": "Enter my world", 54 | "base64": "RW50ZXIgbXkgd29ybGQ=", 55 | "truncated": false 56 | }, 57 | "is_tls": false 58 | }, 59 | "ip": null, 60 | "timestamp": "2020-11-17T21:16:23.847161686+01:00", 61 | "session": "buq2v5oo4skos28gfp20", 62 | "type": "http", 63 | "src_ip": "127.0.0.1", 64 | "dst_port": 10080, 65 | "matches": {}, 66 | "inline_matches": [], 67 | "embedded": {} 68 | } 69 | ``` 70 | 71 | !!! Info 72 | The `errors` field contains the error met while parsing the request body or the Host field. 73 | 74 | ## TCP 75 | ### Rules 76 | |Key|Type|Example| 77 | |---|---|---| 78 | |`tcp.payload`|*complex*|
tcp.payload:
contains:
- "/console/css/%2e"| 79 | |`tcp.flags`|*flags*|tcp.flags:
- "PA"
- "S"| 80 | |`tcp.fragbits`|*flags*|tcp.fragbits:| 81 | |`tcp.dsize`|*number*|
- "M"tcp.dsize: 1234| 82 | |`tcp.seq`|*number*|tcp.seq: 4321| 83 | |`tcp.ack`|*number*|tcp.ack: 0| 84 | |`tcp.window`|*number*|tcp.window: 512| 85 | 86 | TCP flags values : 87 | 88 | |Keyword|Name|Value| 89 | |---|---|---| 90 | |`F`|FIN|0x01| 91 | |`S`|SYN|0x02| 92 | |`R`|RST|0x04| 93 | |`P`|PSH|0x08| 94 | |`A`|ACK|0x10| 95 | |`U`|URG|0x20| 96 | |`E`|ECE|0x40| 97 | |`C`|CWR|0x80| 98 | |`0`|NULL|0x00| 99 | 100 | TCP fragbits values : 101 | 102 | |Keyword|Name|Value| 103 | |---|---|---| 104 | |`M`|More Fragments|0x01| 105 | |`D`|Don't Fragment|0x02| 106 | |`R`|Reserved Bit|0x04| 107 | 108 | ### Log data 109 | 110 | !!! Example 111 | ```json 112 | { 113 | "tcp": { 114 | "window": 512, 115 | "seq": 1906765553, 116 | "ack": 2514263732, 117 | "data_offset": 8, 118 | "flags": "PA", 119 | "urgent": 0, 120 | "payload": { 121 | "content": "I made a discovery today. I found a computer.\n", 122 | "base64": "SSBtYWRlIGEgZGlzY292ZXJ5IHRvZGF5LiAgSSBmb3VuZCBhIGNvbXB1dGVyLgo=", 123 | "truncated": false 124 | } 125 | }, 126 | "ip": { 127 | "version": 4, 128 | "ihl": 5, 129 | "tos": 0, 130 | "length": 99, 131 | "id": 39114, 132 | "fragbits": "DF", 133 | "frag_offset": 0, 134 | "ttl": 64, 135 | "protocol": 6 136 | }, 137 | "timestamp": "2020-11-16T15:50:01.277828+01:00", 138 | "session": "bup9368o4skolf20rt8g", 139 | "type": "tcp", 140 | "src_ip": "127.0.0.1", 141 | "dst_port": 1234, 142 | "matches": {}, 143 | "inline_matches": [], 144 | "embedded": {} 145 | } 146 | ``` 147 | 148 | ## UDP 149 | ### Rules 150 | 151 | |Key|Type|Example| 152 | |---|---|---| 153 | |`udp.payload`|*complex*|udp.payload:
contains:
- "/console/css/%2e"| 154 | |`udp.checksum`|*number*|udp.checksum: 0xfe37| 155 | |`udp.length`|*number*|udp.length: 36| 156 | |`udp.dsize`|*number*|udp.dsize: 28| 157 | 158 | !!! Tip 159 | `udp.dsize` check the payload size, while the `udp.length` check the UDP packet's length. 160 | 161 | ### Log data 162 | 163 | !!! Example 164 | 165 | ```json 166 | { 167 | "udp": { 168 | "payload": { 169 | "content": "I made a discovery today. I found a computer.\n", 170 | "base64": "SSBtYWRlIGEgZGlzY292ZXJ5IHRvZGF5LiBJIGZvdW5kIGEgY29tcHV0ZXIuCg==", 171 | "truncated": false 172 | }, 173 | "length": 54, 174 | "checksum": 65097 175 | }, 176 | "ip": { 177 | "version": 4, 178 | "ihl": 5, 179 | "tos": 0, 180 | "length": 74, 181 | "id": 3230, 182 | "fragbits": "DF", 183 | "frag_offset": 0, 184 | "ttl": 64, 185 | "protocol": 17 186 | }, 187 | "timestamp": "2020-11-17T19:02:12.90819+01:00", 188 | "session": "buq1090o4sktrqnfoe6g", 189 | "type": "udp", 190 | "src_ip": "127.0.0.1", 191 | "dst_port": 1234, 192 | "matches": {}, 193 | "inline_matches": [], 194 | "embedded": {} 195 | } 196 | ``` 197 | 198 | ## ICMPv4 199 | ### Rules 200 | 201 | |Key|Type|Example| 202 | |---|---|---| 203 | |`icmpv4.payload`|*complex*|icmpv4.payload:
contains:
- "the world of the electron and the switch"| 204 | |`icmpv4.typecode`|*number*|icmpv4.typecode: 2048| 205 | |`icmpv4.type`|*number*|icmpv4.type: 0x8| 206 | |`icmpv4.code`|*number*|icmpv4.code: 0| 207 | |`icmpv4.seq`|*number*|icmpv4.seq: 1| 208 | |`icmpv4.checksum`|*number*|icmpv4.checksum: 0x0416| 209 | 210 | ### Log data 211 | 212 | !!! Example 213 | 214 | ```json 215 | { 216 | "icmpv4": { 217 | "type_code": 2048, 218 | "type": 8, 219 | "code": 0, 220 | "type_code_name": "EchoRequest", 221 | "checksum": 10240, 222 | "id": 0, 223 | "seq": 0, 224 | "payload": { 225 | "content": "the world of the electron and the switch", 226 | "base64": "dGhlIHdvcmxkIG9mIHRoZSBlbGVjdHJvbiBhbmQgdGhlIHN3aXRjaA==", 227 | "truncated": false 228 | } 229 | }, 230 | "ip": { 231 | "version": 4, 232 | "ihl": 5, 233 | "tos": 0, 234 | "length": 68, 235 | "id": 1, 236 | "fragbits": "", 237 | "frag_offset": 0, 238 | "ttl": 64, 239 | "protocol": 1 240 | }, 241 | "timestamp": "2020-11-18T12:47:25.101191+01:00", 242 | "session": "n/a", 243 | "type": "icmpv4", 244 | "src_ip": "127.0.0.1", 245 | "dst_port": 0, 246 | "matches": {}, 247 | "inline_matches": [], 248 | "embedded": {} 249 | } 250 | ``` 251 | 252 | ## ICMPv6 253 | ### Rules 254 | 255 | |Key|Type|Example| 256 | |---|---|---| 257 | |`icmpv6.payload`|*complex*|icmpv6.payload:
contains:
- "the world of the electron and the switch"| 258 | |`icmpv6.typecode`|*number*|icmpv6.typecode: 32768| 259 | |`icmpv6.type`|*number*|icmpv6.type: 0x80| 260 | |`icmpv6.code`|*number*|icmpv6.code: 0| 261 | |`icmpv6.checksum`|*number*|icmpv6.checksum: 0x275b| 262 | 263 | ### Log data 264 | 265 | !!! Example 266 | 267 | ```json 268 | { 269 | "icmpv6": { 270 | "type_code": 32768, 271 | "type": 128, 272 | "code": 0, 273 | "type_code_name": "EchoRequest", 274 | "checksum": 44947, 275 | "payload": { 276 | "content": "\u0000\u0000\u0000\u0000the world of the electron and the switch", 277 | "base64": "AAAAAHRoZSB3b3JsZCBvZiB0aGUgZWxlY3Ryb24gYW5kIHRoZSBzd2l0Y2g=", 278 | "truncated": false 279 | } 280 | }, 281 | "ip": { 282 | "version": 6, 283 | "length": 48, 284 | "next_header": 58, 285 | "next_header_name": "ICMPv6", 286 | "traffic_class": 0, 287 | "flow_label": 0, 288 | "hop_limit": 64 289 | }, 290 | "timestamp": "2020-11-18T12:42:47.461931+01:00", 291 | "session": "n/a", 292 | "type": "icmpv6", 293 | "src_ip": "::1", 294 | "dst_port": 0, 295 | "matches": {}, 296 | "inline_matches": [], 297 | "embedded": {} 298 | } 299 | ``` 300 | -------------------------------------------------------------------------------- /internal/rules/raw_rules.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/ma111e/melody/internal/logging" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // RawRules abstracts a group of raw rules in a rule file 14 | type RawRules map[string]RawRule 15 | 16 | // HTTPRule describes the raw "match" section of a rule targeting HTTP 17 | type HTTPRule struct { 18 | URI RawConditions `yaml:"http.uri"` 19 | Body RawConditions `yaml:"http.body"` 20 | Headers RawConditions `yaml:"http.headers"` 21 | Verb RawConditions `yaml:"http.method"` 22 | Proto RawConditions `yaml:"http.proto"` 23 | TLS *bool `yaml:"http.tls"` 24 | Any bool `yaml:"any"` 25 | } 26 | 27 | // ParsedHTTPRule describes the parsed "match" section of a rule targeting HTTP 28 | type ParsedHTTPRule struct { 29 | URI *ConditionsList 30 | Body *ConditionsList 31 | Headers *ConditionsList 32 | Verb *ConditionsList 33 | Proto *ConditionsList 34 | TLS *bool 35 | } 36 | 37 | // TCPRule describes the raw "match" section of a rule targeting TCP 38 | type TCPRule struct { 39 | IPOption RawConditions `yaml:"tcp.ipoption"` 40 | Fragbits RawFragbitsList `yaml:"tcp.fragbits"` 41 | Dsize *uint `yaml:"tcp.dsize"` 42 | Flags RawTCPFlagsList `yaml:"tcp.flags"` 43 | Seq *uint32 `yaml:"tcp.seq"` 44 | Ack *uint32 `yaml:"tcp.ack"` 45 | Payload RawConditions `yaml:"tcp.payload"` 46 | Window *uint16 `yaml:"tcp.window"` 47 | Any bool `yaml:"any"` 48 | } 49 | 50 | // ParsedTCPRule describes the parsed "match" section of a rule targeting TCP 51 | type ParsedTCPRule struct { 52 | IPOption *ConditionsList 53 | Fragbits []*uint8 54 | Flags []*uint8 55 | Dsize *uint 56 | Seq *uint32 57 | Ack *uint32 58 | Window *uint16 59 | Payload *ConditionsList 60 | } 61 | 62 | // ICMPv4Rule describes the raw "match" section of a rule targeting ICMPv4 63 | type ICMPv4Rule struct { 64 | TypeCode *uint16 `yaml:"icmpv4.typecode"` 65 | Type *uint8 `yaml:"icmpv4.type"` 66 | Code *uint8 `yaml:"icmpv4.code"` 67 | Checksum *uint16 `yaml:"icmpv4.checksum"` 68 | Seq *uint16 `yaml:"icmpv4.seq"` 69 | Payload RawConditions `yaml:"icmpv4.payload"` 70 | Any bool `yaml:"any"` 71 | } 72 | 73 | // ParsedICMPv4Rule describes the parsed "match" section of a rule targeting ICMPv4 74 | type ParsedICMPv4Rule struct { 75 | TypeCode *uint16 76 | Type *uint8 77 | Code *uint8 78 | Checksum *uint16 79 | Seq *uint16 80 | Payload *ConditionsList 81 | } 82 | 83 | // ICMPv6Rule describes the raw "match" section of a rule targeting ICMPv6 84 | type ICMPv6Rule struct { 85 | TypeCode *uint16 `yaml:"icmpv6.typecode"` 86 | Type *uint8 `yaml:"icmpv6.type"` 87 | Code *uint8 `yaml:"icmpv6.code"` 88 | Checksum *uint16 `yaml:"icmpv6.checksum"` 89 | Payload RawConditions `yaml:"icmpv6.payload"` 90 | Any bool `yaml:"any"` 91 | } 92 | 93 | // ParsedICMPv6Rule describes the parsed "match" section of a rule targeting ICMPv6 94 | type ParsedICMPv6Rule struct { 95 | TypeCode *uint16 96 | Type *uint8 97 | Code *uint8 98 | Checksum *uint16 99 | Payload *ConditionsList 100 | } 101 | 102 | // UDPRule describes the raw "match" section of a rule targeting UDP 103 | type UDPRule struct { 104 | Length *uint16 `yaml:"udp.length"` 105 | Dsize *uint `yaml:"udp.dsize"` 106 | Checksum *uint16 `yaml:"udp.checksum"` 107 | Payload RawConditions `yaml:"udp.payload"` 108 | Any bool `yaml:"any"` 109 | } 110 | 111 | // ParsedUDPRule describes the parsed "match" section of a rule targeting UDP 112 | type ParsedUDPRule struct { 113 | Length *uint16 114 | Dsize *uint 115 | Checksum *uint16 116 | Payload *ConditionsList 117 | } 118 | 119 | // Filters groups the exposed rule filters 120 | type Filters struct { 121 | Ports []string `yaml:"ports"` 122 | IPs []string `yaml:"ips"` 123 | } 124 | 125 | // Metadata describes the exposed content of the "meta" field 126 | type Metadata struct { 127 | ID string `yaml:"id"` 128 | Status string `yaml:"status"` 129 | Description string `yaml:"description"` 130 | Author string `yaml:"author"` 131 | Created string `yaml:"created"` 132 | Modified string `yaml:"modified"` 133 | References []string `yaml:"references"` 134 | } 135 | 136 | // RawRule describes the format of a rule as written by the user 137 | type RawRule struct { 138 | Whitelist Filters `yaml:"whitelist"` 139 | Blacklist Filters `yaml:"blacklist"` 140 | 141 | Match interface{} `yaml:"match"` 142 | 143 | Tags map[string]string `yaml:"tags"` 144 | Layer string `yaml:"layer"` 145 | IPProtocol RawConditions `yaml:"ip_protocol"` 146 | 147 | Metadata Metadata `yaml:"meta"` 148 | Additional map[string]string `yaml:"embed"` 149 | } 150 | 151 | var ( 152 | validMatchKeysMap map[string]interface{} = LoadValidMatchKeysMap() 153 | ) 154 | 155 | // Parse creates a Rule from a RawRule 156 | func (rawRule RawRule) Parse() (Rule, error) { 157 | var err error 158 | rule := NewRule(rawRule) 159 | 160 | if rawRule.Match == nil { 161 | return rule, nil 162 | } 163 | 164 | rawMatch, err := yaml.Marshal(rawRule.Match) 165 | if err != nil { 166 | logging.Errors.Printf("failed to parse rule '%s' : invalid yaml definition (%s)", rawRule.Metadata.ID, err) 167 | // Fatal error 168 | os.Exit(1) 169 | } 170 | 171 | for key := range rawRule.Match.(map[string]interface{}) { 172 | // "any" is the only valid non-layer specific property in the "match" block : 173 | // It can be either "any" or. 174 | if key != "any" && !strings.HasPrefix(key, rawRule.Layer+".") { 175 | return Rule{}, fmt.Errorf("property '%s' is not supported with layer '%s'", key, rawRule.Layer) 176 | } 177 | 178 | if _, ok := validMatchKeysMap[key]; !ok { 179 | return Rule{}, fmt.Errorf("unknown property '%s'", key) 180 | } 181 | } 182 | 183 | switch rawRule.Layer { 184 | case "http": 185 | var buf HTTPRule 186 | 187 | err = yaml.Unmarshal(rawMatch, &buf) 188 | if err != nil { 189 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 190 | } 191 | 192 | parsedURI, err := buf.URI.ParseList() 193 | if err != nil { 194 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 195 | } 196 | 197 | parsedBody, err := buf.Body.ParseList() 198 | if err != nil { 199 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 200 | } 201 | 202 | parsedVerb, err := buf.Verb.ParseList() 203 | if err != nil { 204 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 205 | } 206 | 207 | parsedHeaders, err := buf.Headers.ParseList() 208 | if err != nil { 209 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 210 | } 211 | 212 | parsedProto, err := buf.Proto.ParseList() 213 | if err != nil { 214 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 215 | } 216 | 217 | rule.HTTP = ParsedHTTPRule{ 218 | URI: parsedURI, 219 | Body: parsedBody, 220 | Verb: parsedVerb, 221 | Headers: parsedHeaders, 222 | Proto: parsedProto, 223 | TLS: buf.TLS, 224 | } 225 | 226 | rule.MatchAll = !buf.Any 227 | 228 | case "tcp": 229 | var buf TCPRule 230 | 231 | err = yaml.Unmarshal(rawMatch, &buf) 232 | if err != nil { 233 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 234 | } 235 | 236 | parsedIPOption, err := buf.IPOption.ParseList() 237 | if err != nil { 238 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 239 | } 240 | 241 | parsedPayload, err := buf.Payload.ParseList() 242 | if err != nil { 243 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 244 | } 245 | 246 | rule.TCP = ParsedTCPRule{ 247 | IPOption: parsedIPOption, 248 | Fragbits: buf.Fragbits.ParseList(), 249 | Flags: buf.Flags.ParseList(), 250 | Window: buf.Window, 251 | Dsize: buf.Dsize, 252 | Seq: buf.Seq, 253 | Ack: buf.Ack, 254 | Payload: parsedPayload, 255 | } 256 | 257 | rule.MatchAll = !buf.Any 258 | 259 | case "udp": 260 | var buf UDPRule 261 | 262 | err = yaml.Unmarshal(rawMatch, &buf) 263 | if err != nil { 264 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 265 | } 266 | 267 | parsedPayload, err := buf.Payload.ParseList() 268 | if err != nil { 269 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 270 | } 271 | 272 | rule.UDP = ParsedUDPRule{ 273 | Dsize: buf.Dsize, 274 | Length: buf.Length, 275 | Checksum: buf.Checksum, 276 | Payload: parsedPayload, 277 | } 278 | 279 | rule.MatchAll = !buf.Any 280 | 281 | case "icmpv4": 282 | var buf ICMPv4Rule 283 | 284 | err = yaml.Unmarshal(rawMatch, &buf) 285 | if err != nil { 286 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 287 | } 288 | 289 | parsedPayload, err := buf.Payload.ParseList() 290 | if err != nil { 291 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 292 | } 293 | 294 | rule.ICMPv4 = ParsedICMPv4Rule{ 295 | TypeCode: buf.TypeCode, 296 | Type: buf.Type, 297 | Code: buf.Code, 298 | Checksum: buf.Checksum, 299 | Seq: buf.Seq, 300 | Payload: parsedPayload, 301 | } 302 | 303 | rule.MatchAll = !buf.Any 304 | 305 | case "icmpv6": 306 | var buf ICMPv6Rule 307 | 308 | err = yaml.Unmarshal(rawMatch, &buf) 309 | if err != nil { 310 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 311 | } 312 | 313 | parsedPayload, err := buf.Payload.ParseList() 314 | if err != nil { 315 | return Rule{}, fmt.Errorf("failed to parse rule '%s' : %s", rawRule.Metadata.ID, err) 316 | } 317 | 318 | rule.ICMPv6 = ParsedICMPv6Rule{ 319 | TypeCode: buf.TypeCode, 320 | Type: buf.Type, 321 | Code: buf.Code, 322 | Checksum: buf.Checksum, 323 | Payload: parsedPayload, 324 | } 325 | 326 | rule.MatchAll = !buf.Any 327 | } 328 | 329 | return rule, nil 330 | } 331 | -------------------------------------------------------------------------------- /docs/docs/rules.md: -------------------------------------------------------------------------------- 1 | Melody rules are used to apply tags on matching packets. They have multiple use cases, such as monitoring emerging threats, automated droppers, vulnerability scanners... 2 | 3 | Take a look in the `$melody/rule-available` and `$melody/internal/rules/test_resources` folders to quickly find working examples. 4 | 5 | ## First look 6 | 7 | A rule file can contain multiple rule descriptions. 8 | 9 | !!! Example 10 | This example detects CVE-2020-14882 (Oracle Weblogic RCE) scans or exploitation attempts by matching either of the two URI on the HTTP level : 11 | 12 | ```yaml 13 | CVE-2020-14882 Oracle Weblogic Server RCE: 14 | layer: http 15 | meta: 16 | id: 3e1d86d8-fba6-4e15-8c74-941c3375fd3e 17 | version: 1.0 18 | author: BonjourMalware 19 | status: stable 20 | created: 2020/11/07 21 | modified: 2020/20/07 22 | description: "Checking or trying to exploit CVE-2020-14882" 23 | references: 24 | - "https://nvd.nist.gov/vuln/detail/CVE-2020-14882" 25 | match: 26 | http.uri: 27 | startswith|any|nocase: 28 | - "/console/css/" 29 | - "/console/images" 30 | contains|any|nocase: 31 | - "console.portal" 32 | - "consolejndi.portal?test_handle=" 33 | tags: 34 | cve: "cve-2020-14882" 35 | vendor: "oracle" 36 | product: "weblogic" 37 | impact: "rce" 38 | ``` 39 | 40 | !!! Tip 41 | Check the [the `whitelist` and `blacklist` section](#whitelist-and-blacklist) to filter ports and IP addresses. 42 | 43 | ## Structure 44 | 45 | The rules have 7 sections : `layer`, `meta`, `match`, `whitelist`, `blacklist`, `tags` and `embed`. 46 | 47 | ### layer 48 | The rule will look for matches in the specified `layer`'s protocol data. 49 | 50 | Each `layer` expose different fields depending on the protocol they represent. They're detailed in the [Layers](/layers/) page. 51 | 52 | The following layers are supported : 53 | 54 | |Key|IPv4|IPv6| 55 | |---|---|---| 56 | |http|✅|✅| 57 | |tcp|✅|✅| 58 | |udp|✅|✅| 59 | |icmpv4|✅|❌| 60 | |icmpv6|❌|✅| 61 | 62 | !!! important 63 | A single rule only applies to the targeted layer. Use multiple rules if you want to match multiple layers. 64 | 65 | ### meta 66 | The `meta` section contains all the rule's metadata. Every keys are mandatory, except `references`. 67 | 68 | |Key|Type|Description|Values|Examples| 69 | |---|---|---|---|---| 70 | |**id**|*string*|Rule's unique identifier. Each rule must have a unique UUIDv4|-| id: c30370f7-aaa8-41d0-a392-b56c94869128| 71 | |**version**|*string*|Rule syntax version|1.0|version: 1.0| 72 | |**author**|*string*|The name of the rule's author|-|author: BonjourMalware| 73 | |**status**|*string*|The status gives an indication of the usability of the rule|stable, experimental|status: stable| 74 | |**created**|*yyyy/mm/dd*|Creation date|-|created: 2020/11/07| 75 | |**modified**|*yyyy/mm/dd*|Last modification date|-|modified: 2020/11/07| 76 | |**description**|*string*|A quick description of what the rule is attempting to match|-|description: Checking or trying to exploit CVE-2020-14882| 77 | |**references**|*array*|The status gives an indication of the usability of the rule|-|references:| 78 | 79 | !!! Important 80 | You must generate a new UUIDv4 for the `id` of every rule you create. 81 | 82 | Sample code for Python : 83 | ```python 84 | import uuid 85 | 86 | print(uuid.uuid4()) 87 | ``` 88 | 89 | Go ([playground](https://play.golang.org/p/9qDBHpZ2QqY)) : 90 | ```go 91 | package main 92 | 93 | import ( 94 | "fmt" 95 | "github.com/google/uuid" 96 | ) 97 | 98 | func main(){ 99 | fmt.Println(uuid.New()) 100 | } 101 | ``` 102 | 103 | ### match 104 | The `match` block contains a set of *conditions* that will be checked on every packet of the rule's `layer` type. 105 | 106 | Here is the structure of the `match` section : 107 | 108 | ```yaml 109 | match: 110 | any: [true|false] # false by default 111 | field1: # complex condition 112 | any: [true|false] # false by default 113 | operator1|modifier1|modifier2: # matching operator with its modifiers 114 | - value1 115 | - value2 116 | operator2: 117 | - value 118 | field2: # array condition 119 | - value1 120 | - value2 121 | field3: value # string or number condition 122 | ``` 123 | 124 | #### Conditions 125 | A *condition* corresponds to a field in a packet, specified by its name. 126 | 127 | The available *conditions* depends on the `layer` key. The keys are namespaced according to the type they belong to. 128 | 129 | !!! Example 130 | `udp.payload`, `tcp.flags`, `http.uri`... 131 | 132 | There are 3 types of *conditions* : `number`, `flags` or `complex`. 133 | 134 | ##### Number 135 | A number. 136 | 137 | !!! Example 138 | ```yaml 139 | tcp.window: 512 140 | ``` 141 | 142 | !!! Note 143 | The `number` types takes advantage of YAML to support octal (0o1234), hex (0x1234) and decimal (1234) representation. 144 | 145 | ##### Flags 146 | 147 | `flags` *condition* are made of a list of flag combination to match. 148 | 149 | The *condition* is valid as soon as a match is found (OR). 150 | 151 | !!! Example 152 | ``` 153 | tcp.flags: 154 | - "PA" 155 | - "S" 156 | ``` 157 | 158 | This rule will match a TCP packet with its flag bits set to "PA" (PSH-ACK, 0x18) or "S" (SYN, 0x2). 159 | 160 | !!! Note 161 | Only two fields support `flags` *condition* : `tcp.flags` and `tcp.fragbits`. 162 | 163 | ##### Complex 164 | 165 | The `complex` *condition* type supports *matching operators* and inline *modifiers*. 166 | 167 | To check which fields support `complex` *conditions*, take a look at the [layers documentation](/layers). 168 | 169 | ###### Matching operators 170 | The *matching operator* specifies how to handle data. 171 | 172 | A single *condition* can be made of a set of *matching operators*. 173 | 174 | !!! Important 175 | By default, a rule needs to validate all the *conditions* to match. However, you can specify `any: true` to force a rule to test all of its conditions and return a match as soon as it find a valid one. 176 | 177 | !!! Example 178 | ```yaml 179 | udp.payload: 180 | contains: 181 | - "after all, we're all alike." 182 | startswith: 183 | - "Damn kids" 184 | any: true 185 | ``` 186 | 187 | In this example, the *condition* key is `udp.payload` and the *matching operators* are `contains` and `startswith`. 188 | 189 | This rule will match if the payload of an UDP packet startswith the string "Damn kids" OR contains "after all, we're all alike.". 190 | 191 | The rule needs both to match if we remove the `any: true` option. 192 | 193 | |Name|Description| 194 | |---|---| 195 | |is|The packet's field value is **strictly equal** to the *condition*'s value| 196 | |contains|The packet's field value **contains** the *condition*'s value| 197 | |startswith|The packet's field value **starts** with the *condition*'s value| 198 | |endswith|The packet's field value **ends** with the *condition*'s value| 199 | 200 | ###### Modifiers 201 | *Modifiers* are a way to quickly set options for the *matching operator*. 202 | 203 | They live on the same line, split by `|`. All *modifiers* can be mixed at once. 204 | 205 | !!! Important 206 | By default, a *condition* needs to match all of the given values (AND). However, you can use the `|any` *modifier* to reverse it and force it to test all the values and to return on the first match. 207 | 208 | !!! Example 209 | ```yaml 210 | http.body: 211 | contains|any|nocase: 212 | - "Enter my world" 213 | - "the beauty of the baud" 214 | ``` 215 | 216 | In this example, the *modifiers* are `any` and `nocase`. This rule will match if the URI field of an HTTP packet contains any item in the list. 217 | 218 | |Name|Description|Example| 219 | |---|---|---| 220 | |any|The rule match if **any** of the values in the list matches|-| 221 | |nocase|The match is **case insensitive**|abcd == aBcD == ABCD| 222 | |regex|The value is a **regular expression**|'(?:[0-9]{1,3}\.){3}[0-9]{1,3}' == 192.0.2.1| 223 | 224 | !!! Danger 225 | Although the regex is compiled only once, it can cause severe overhead while matching packets. Use it with caution. 226 | 227 | ###### Hybrid pattern 228 | 229 | `complex` *condition*'s support hex values by wrapping them between two `|`. 230 | 231 | You can mix hex and ascii in a single string as well. 232 | 233 | !!! Example 234 | ```yaml 235 | http.body: 236 | contains: 237 | - "|45 6e 74 65 72206d79| world" 238 | ``` 239 | 240 | !!! Note 241 | '0x' hex notation (`|0xbe 0xef|`) is invalid. You can mix spaced and not spaced hex bytes though. 242 | 243 | 244 | ### tags 245 | Each of the key/value pair in the `tags` object will be appended to the `matches` field of each of the matching packets. 246 | 247 | ### embed 248 | This is a block where the user can will embed any data in the `embedded` key of the matching packet. It can be used as an alternative to `tags` to add contextual information. 249 | 250 | !!! Example 251 | ```yaml 252 | embed: 253 | my_crime: "curiosity" 254 | ... 255 | ``` 256 | 257 | ### whitelist and blacklist 258 | 259 | These two fields can be used to filter the packets on which the rule is applied. 260 | 261 | IP source addresses and ports are supported. 262 | 263 | #### IP address 264 | 265 | !!! Example 266 | ```yaml 267 | whitelist: 268 | ips: 269 | - 127.0.0.1 270 | ``` 271 | 272 | This example only tries to match the packets coming from 127.0.0.1. 273 | 274 | ```yaml 275 | blacklist: 276 | ips: 277 | - 127.0.0.1 278 | ``` 279 | 280 | Use the blacklist keyword to reverse the logic and apply the rule to all packets but the one coming from 127.0.0.1. 281 | 282 | !!! Example 283 | ```yaml 284 | whitelist: 285 | ips: 286 | - 127.0.0.0/24 287 | ``` 288 | 289 | CIDR notation supported. 290 | 291 | #### Ports 292 | 293 | !!! Example 294 | ```yaml 295 | whitelist: 296 | ports: 297 | - 80 298 | ``` 299 | 300 | This example only tries to match the packets going to port 80 . 301 | 302 | ```yaml 303 | blacklist: 304 | ports: 305 | - 80 306 | ``` 307 | 308 | Use the blacklist keyword to reverse the logic and apply the rule to all packets but the one going to port 80. 309 | 310 | !!! Example 311 | ```yaml 312 | whitelist: 313 | ports: 314 | - 8000 - 9000 315 | ``` 316 | 317 | Port ranges are supported. You can choose to put spaces or not. 318 | --------------------------------------------------------------------------------
- "https://nvd.nist.gov/vuln/detail/CVE-2020-14882"
- "https://github.com/jas502n/CVE-2020-14882"
- ...